diff --git a/backend/static/export-viewer/index.html b/backend/static/export-viewer/index.html index b0f8c56..7283b84 100644 --- a/backend/static/export-viewer/index.html +++ b/backend/static/export-viewer/index.html @@ -6,7 +6,7 @@ EventSnap — Galerie - +
diff --git a/frontend/export-viewer/vite.standalone.config.js b/frontend/export-viewer/vite.standalone.config.js index af9ba95..dc6d1c2 100644 --- a/frontend/export-viewer/vite.standalone.config.js +++ b/frontend/export-viewer/vite.standalone.config.js @@ -3,6 +3,61 @@ import tailwindcss from '@tailwindcss/vite'; import { viteSingleFile } from 'vite-plugin-singlefile'; import { defineConfig } from 'vite'; import { fileURLToPath } from 'node:url'; +import { readFileSync } from 'node:fs'; + +/** Webfonts the shared theme declares, and where their bytes actually live. */ +const FONTS = ['Inter', 'Fraunces']; + +/** + * Inline the webfonts the shared theme CSS references by ABSOLUTE path. + * + * `src/tailwind-theme.css` is the design-token source of truth for both the live app and this + * viewer, and it declares `src: url('/fonts/Inter.woff2')`. That is correct for the app, which + * serves `static/fonts/` from the site root — but the keepsake is opened from `file://` off a USB + * stick or a Downloads folder, where `/fonts/...` resolves to the root of the guest's DISK. Both + * requests 404, silently: `font-display: swap` means the viewer renders in a fallback system font + * with no error, so nothing on the server side can ever report it. The keepsake is the one artifact + * the whole event exists to produce, and it was shipping without the typography it was designed in. + * + * Fixing it in the shared CSS would break the app (a data URI there would inline ~154 KB into every + * page load for no reason), and shipping a `fonts/` folder beside `index.html` would give the guest + * a directory they can break by moving one file. So the substitution belongs HERE, in the build that + * knows its output has no origin: rewrite the emitted HTML only. + * + * Runs in `generateBundle` rather than `transformIndexHtml` because `viteSingleFile` inlines the + * stylesheet during the latter — the `url()` we need to rewrite does not exist in the HTML until + * after it has run. + */ +function inlineThemeFonts() { + return { + name: 'eventsnap:inline-theme-fonts', + enforce: 'post', + generateBundle(_options, bundle) { + const html = bundle['index.html']; + if (!html || typeof html.source !== 'string') return; + + for (const family of FONTS) { + const bytes = readFileSync( + fileURLToPath(new URL(`../static/fonts/${family}.woff2`, import.meta.url)) + ); + const uri = `data:font/woff2;base64,${bytes.toString('base64')}`; + const before = html.source; + html.source = html.source.replaceAll(`/fonts/${family}.woff2`, uri); + // A silent no-op here is the exact failure this plugin exists to prevent, and it + // would come back the moment the theme renames a font or switches to a CDN. Fail + // the build instead of shipping another keepsake in Times New Roman. + if (html.source === before) { + this.error( + `inline-theme-fonts: no reference to /fonts/${family}.woff2 in the built ` + + `keepsake. The shared theme CSS changed how it loads webfonts — update ` + + `FONTS in vite.standalone.config.js to match, or the offline viewer will ` + + `render in a fallback font.` + ); + } + } + } + }; +} // Builds the keepsake viewer as ONE self-contained index.html (all JS + CSS // inlined) so it renders when opened via file://. Uses the plain Svelte plugin @@ -12,7 +67,8 @@ export default defineConfig({ plugins: [ tailwindcss(), svelte({ configFile: false, preprocess: vitePreprocess(), compilerOptions: { runes: true } }), - viteSingleFile() + viteSingleFile(), + inlineThemeFonts() ], resolve: { alias: { $lib: fileURLToPath(new URL('./src/lib', import.meta.url)) }