38c3df8187
Build-time image optimization plus auth-gated private content. - <Image> (src/lib/components/Image.svelte): wraps @sveltejs/enhanced-img for public images under src/lib/assets/images/ (AVIF/WebP, multiple widths, lazy by default), plus a `private` mode for auth-gated images. - Private images: scripts/build-private-images.ts encodes sources from src/lib/assets/private-images/ into private-assets/ (outside the bundle) and a manifest; served only via the auth-checked /private-images/ endpoint (X-Accel-Redirect in prod, disk read in dev). - HikeImage gains a `src` prose mode: build-hikes encodes non-waypoint images referenced in .svx and exposes them by filename (imagesByName); a `private` attr routes them through the gated /hikes/<slug>/private/ path. - <Private> (src/lib/components/Private.svelte): renders prose only to logged-in viewers (cosmetic gating — text still ships in the bundle). - deploy.sh rsyncs private-assets/; prod needs an nginx internal /protected-images/ location.
104 lines
3.2 KiB
TypeScript
104 lines
3.2 KiB
TypeScript
import { sveltekit } from '@sveltejs/kit/vite';
|
|
import { enhancedImages } from '@sveltejs/enhanced-img';
|
|
import { defineConfig, type Plugin } from 'vite';
|
|
import { createReadStream, promises as fs } from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
/** In `vite dev`, hike image binaries live in `hikes-assets/` (outside `/static`
|
|
* so they aren't bundled into the Node build). In production nginx serves them
|
|
* directly from `/var/www/static/hikes/`; the SvelteKit dev server has no
|
|
* nginx in front, so we intercept `/hikes/<slug>/images/<file>` here and
|
|
* stream the file off disk. Private images (`/hikes/<slug>/private/<file>`)
|
|
* intentionally fall through to the SvelteKit endpoint, which enforces auth. */
|
|
function hikeImagesDevPlugin(): Plugin {
|
|
const ROOT = path.resolve(process.cwd(), 'hikes-assets');
|
|
const MIME: Record<string, string> = {
|
|
'.avif': 'image/avif',
|
|
'.webp': 'image/webp',
|
|
'.jpg': 'image/jpeg',
|
|
'.jpeg': 'image/jpeg',
|
|
'.png': 'image/png',
|
|
'.svg': 'image/svg+xml'
|
|
};
|
|
return {
|
|
name: 'hike-images-dev',
|
|
apply: 'serve',
|
|
configureServer(server) {
|
|
server.middlewares.use(async (req, res, next) => {
|
|
const url = req.url ?? '';
|
|
const m = url.match(/^\/hikes\/([^/]+)\/images\/([^/?#]+)(?:[?#].*)?$/);
|
|
if (!m) return next();
|
|
// Slug and filename ship URL-encoded (e.g. "ü" → "%C3%BC"),
|
|
// but the on-disk directory uses the raw UTF-8 character.
|
|
// Decode before joining, else everything under a slug with
|
|
// non-ASCII characters 404s in dev.
|
|
let slug: string, file: string;
|
|
try {
|
|
slug = decodeURIComponent(m[1]);
|
|
file = decodeURIComponent(m[2]);
|
|
} catch {
|
|
return next();
|
|
}
|
|
if (slug.includes('..') || file.includes('..')) return next();
|
|
const filePath = path.join(ROOT, slug, 'images', file);
|
|
try {
|
|
const stat = await fs.stat(filePath);
|
|
const mime = MIME[path.extname(file).toLowerCase()] ?? 'application/octet-stream';
|
|
res.setHeader('Content-Type', mime);
|
|
res.setHeader('Content-Length', String(stat.size));
|
|
res.setHeader('Cache-Control', 'public, max-age=3600');
|
|
createReadStream(filePath).pipe(res);
|
|
} catch {
|
|
next();
|
|
}
|
|
});
|
|
}
|
|
};
|
|
}
|
|
|
|
export default defineConfig({
|
|
css: {
|
|
lightningcss: {
|
|
targets: {
|
|
chrome: (80 << 16),
|
|
firefox: (80 << 16),
|
|
safari: (14 << 16),
|
|
}
|
|
}
|
|
},
|
|
server: {
|
|
allowedHosts: ["bocken.org"]
|
|
},
|
|
// enhancedImages() powers <Image> (src/lib/components/Image.svelte): it runs
|
|
// vite-imagetools (sharp) over every image under src/lib/assets/images/ at
|
|
// build time, emitting AVIF/WebP at multiple widths. Must come before the
|
|
// SvelteKit plugin.
|
|
plugins: [enhancedImages(), hikeImagesDevPlugin(), sveltekit()],
|
|
optimizeDeps: {
|
|
exclude: ['barcode-detector']
|
|
},
|
|
build: {
|
|
rolldownOptions: {
|
|
output: {
|
|
manualChunks: (id) => {
|
|
// Separate large dependencies into their own chunks
|
|
if (id.includes('node_modules')) {
|
|
if (id.includes('chart.js')) {
|
|
return 'chart';
|
|
}
|
|
if (id.includes('@auth/sveltekit')) {
|
|
return 'auth';
|
|
}
|
|
if (id.includes('barcode-detector') || id.includes('zxing-wasm')) {
|
|
return 'barcode';
|
|
}
|
|
if (id.includes('/leaflet/')) {
|
|
return 'leaflet';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|