perf: self-host Inter and stop copying what can be linked - #69
Merged
Conversation
Inter came from fonts.googleapis.com on every page view, sending each reader's IP and User-Agent to a third party and leaving the deployment with one external dependency it otherwise does not have — mermaid is already vendored for exactly this reason. It is now served from this origin via @fontsource-variable/inter, declared in marketplace.css. Only the two latin subsets, upright, are declared, which is what the Google request asked for; importing the package's own CSS would emit the cyrillic, greek and vietnamese faces into dist/ as well. That lets the CSP drop both third-party hosts: style-src and font-src are now 'self' (plus data: and the existing 'unsafe-inline'). nginx.headers.conf and the Express mirror move together, as they must. Alongside it, the build-time costs from #51: - Sub-app assets are hardlinked from apps/ into public/ instead of copied, falling back to a copy when the filesystem will not link. CSS is deliberately still copied: the url() rewrite edits the destination in place and a link would write that back into the artifact. - getStaticPaths no longer attaches the whole resolved registry to every route. The masthead names one app and the catchall titles the page with it, so each page carries only that app's card. - collectHtmlFiles and copyAssets read directory entries with withFileTypes instead of a statSync per file, which also means a symlink is seen as one rather than followed. - loadRegistry memoises on the mtimes of apps.json and .single-page.json, so a repeated getStaticPaths costs two statSync calls rather than two reads and parses, and astro dev still notices an edit. Two items in that issue were already fixed: copyDir moved to scripts/artifacts.js with withFileTypes when it was hardened, and fetch-apps.js no longer dynamically imports copyFileSync. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QqFK6yffibtCBTF8xZ4hXW
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The typeface stops being a third-party request, and the build stops paying for work it can skip.
Changes
Self-hosted Inter (#54)
Every page loaded Inter from
fonts.googleapis.com, which sent each reader's IP and User-Agent to Google on every page view and left the deployment with one external dependency it otherwise does not have — mermaid is already vendored into each artifact for exactly this reason.@fontsource-variable/interadded;src/styles/marketplace.cssdeclares the faces and--font-sansleads with'Inter Variable'.dist/too; theunicode-rangevalues are the package's, so another script still degrades to the fallback stack rather than rendering wrong. Two hashedwoff2files land indist/_astro/, andfont-display: swapis preserved.<link>tags are gone fromBase.astro.style-srcandfont-srcno longer allowlist any external origin.nginx.headers.confand thetests/fragment-server.mjsmirror move together, andtests/nginx-config.spec.jsgains an assertion that neither directive names a host again.Build-time costs (#51)
apps/{slug}/intopublic/{slug}/, with a copy fallback when the filesystem will not link (EXDEV, no hardlink support). CSS is deliberately still a real copy: theurl()rewrite edits the destination in place, and a link would write that edit straight back into the extracted artifact. There is a test for that specifically.getStaticPathsreturns one entry per HTML file and each carried the whole resolved registry, so the route table grew with apps × pages × apps. Each page now carries only its own app's{slug, name, icon};MastheadtakesactiveAppinstead ofapps+activeSlug. To be precise about the win: these props are never serialised into the static HTML, so this is build memory, not output bytes.withFileTypesincollectHtmlFilesandcopyAssets— the entry type comes from the directory read that already happened instead of astatSyncper file, and a symlink is reported as one rather than followed.loadRegistrymemoises, keyed on the mtimes ofapps.jsonandapps/.single-page.jsonrather than oncwdalone, so a repeatedgetStaticPathscosts twostatSynccalls instead of two reads and parses whileastro devstill picks up an edit.Two items in #51 were already fixed and needed no change:
copyDirmoved toscripts/artifacts.jswithwithFileTypeswhen artifact extraction was hardened, andscripts/fetch-apps.jsno longer dynamically importscopyFileSync.Verification
npm run build:headless— green, 8 apps; exactly twowoff2indist/_astro/, no Google Fonts origin anywhere in the outputnpm test— 85 passed (was 82)npx playwright test --config=playwright.config.ci.js— 22 passednpm run test:container— 28 passed against a freshly built image, which is what proves the tightened CSP still renders (the in-browser CSP tests would catch a blocked font)npm run selftest(actions/publish-single-page-docs) — 20 passednpm audit --omit=dev --audit-level=high— 0 vulnerabilitiesHardlinking verified directly:
nlink=2on copied assets,nlink=1on rewritten CSS, andapps/…/depth-check.cssstill holding its originalurl(/fonts/…).Closes #51
Closes #54