fix(export-viewer): inline the webfonts so the offline keepsake is self-contained
The viewer inherits `src: url('/fonts/Inter.woff2')` from the shared theme CSS. 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, and `font-display: swap` makes it silent:
the viewer renders in a fallback system font with nothing server-side able to report it.
Found by opening a real released keepsake in a browser and watching `requestfailed` — no
other signal exists, which is the recurring lesson about this artifact.
Fixing it in the shared CSS would inline ~154 KB into every app page load for nothing, and
shipping a `fonts/` folder beside index.html gives the guest a directory they can break by
moving one file. So the substitution belongs in the build that knows its output has no
origin. The plugin errors the build if the theme ever stops referencing those URLs, rather
than silently shipping another keepsake in Times New Roman.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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)) }
|
||||
|
||||
Reference in New Issue
Block a user