Serve CanvasKit and Roboto from the app, and make a release one edit - #103
Merged
Conversation
Two files were fetched from Google on every page load: CanvasKit from gstatic.com and Roboto from fonts.gstatic.com. Both are Flutter Web defaults rather than choices anyone made here, and both sit badly in an app whose claim is that a CV never leaves the device — index.html already sets dartPdfJsBaseUrl locally for exactly that reason, and these two were simply missed. CanvasKit costs nothing to bring in-house: `flutter build web` already copies it into build/web, so those bytes were being deployed to Pages and then ignored in favour of a CDN round trip. The loader now points at them. Roboto is registered as a `fonts:` family from the four faces already bundled for the PDF, so CanvasKit finds it in the asset manifest instead of downloading it. The app now boots and renders with every non-origin request blocked, which it could not do before. Not request-free, and the comments say so rather than overclaiming: a screen showing a RegionPreset flag still pulls Noto Color Emoji, because the flags are emoji and no embedded font covers them. Closing that means bundling ~10MB or drawing the flags as assets; neither belongs here. All eight golden baselines move. The bundled Roboto is a different cut from the SDK's, which is what golden_toolkit was loading, so every glyph rasterizes a hair differently — layout, line breaks and positions are identical, checked against the diff rather than assumed. The new baselines are the more honest ones: they use the font that now ships. A release is one edit again. `BackupService._appVersion` was a second literal of pubspec's `version:`, kept in step by a test — which catches drift but does nothing about the failure that actually happened twice this week, a merge with no bump at all and so no deploy. It now reads a constant `build_version` generates from pubspec. A dev dependency producing a checked-in constant, not a runtime one: the reason package_info_plus was declined still holds, since nothing reads this back and the app should not carry a plugin to learn its own version. The test survives, re-aimed at a stale generated file, and was confirmed to fail when handed one. 2.23.0 covers this and the markdown emphasis work in #102, which merged without a bump and so is on main but was never published. Also adds tool/serve_web.mjs and a run-app skill. Running this headlessly needed four workarounds; two are now fixed above, and the other two are worth writing down rather than rediscovering: Flutter's engine throws RangeError from the JS Intl.Locale constructor when a headless browser reports "en-US@posix", and usePathUrlStrategy 404s on refresh under a plain static server. .claude/ stays ignored for local config, but skills are the opposite of machine-specific, so they are shared now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014kPZNZ5pCETb2mHRaz25Kz
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 and why
Trying to run this app headlessly took four workarounds. Two of them turned
out to be the app's problem, not the container's.
Every page load fetched two files from Google. CanvasKit from
gstatic.com, Roboto fromfonts.gstatic.com. Neither is a choice anyonemade here — they are Flutter Web defaults — but both sit badly in an app
whose whole claim is that a CV never leaves the device.
index.htmlalready sets
dartPdfJsBaseUrlto a local path for exactly that reason;these two were simply missed.
CanvasKit costs nothing to bring in-house:
flutter build webalreadycopies it into
build/web, so those bytes were being deployed to Pages andthen ignored in favour of a CDN round trip. The loader now points at them.
Roboto is registered as a
fonts:family from the four faces alreadybundled for the PDF, so CanvasKit finds it in the asset manifest instead of
downloading it.
The app now boots and renders with every non-origin request blocked,
which it could not do before. That is the check, not an inference.
A release is one edit again.
BackupService._appVersionwas a secondliteral of pubspec's
version:, kept in step by a test. That catchesdrift, but does nothing about the failure that actually happened twice this
week: a merge with no bump at all, so
deploy.ymlskipped and the worknever shipped. It now reads a constant
build_versiongenerates frompubspec.
How it was verified
dart format --output=none --set-exit-if-changed .flutter analyzeflutter test --exclude-tags=golden— and the full run, 889passing including goldens
On the browser box: the app was driven headlessly with a Playwright route
handler that aborts and records any request not to
localhost. Bootand first render report
EXTERNAL REQUESTS: NONEand no page errors, withtext rendering correctly — so the font really is coming from the bundle.
Repeated after merging
mainin, with the same result.Goldens
All eight move, and that is expected. The bundled Roboto is a different
cut from the Flutter SDK's, which is what
loadAppFonts()was feeding thegoldens, so every glyph rasterizes a hair differently. I compared master
against test before blessing anything: layout, line breaks and element
positions are pixel-identical, and only glyph interiors differ (largest
diff 1.41% on
vault_view_cv_defaults_open, smallest 0.05%). The newbaselines are the more honest ones — they use the font that now ships.
Generated on this Linux container rather than via
update-goldens.yml,same as the previous PR; CI's
Goldenjob is what actually adjudicatesthem, and it passed there last time.
Anything else
This does not make the app request-free, and the comments say so. A
screen showing a
RegionPresetflag still pulls Noto Color Emoji, becausethe flags are emoji and no embedded font covers them — it fires on the
Vault list, the CV-defaults panel, the region gallery and the drafts cards.
Closing it means bundling a ~10MB emoji font or drawing the flags as
assets. Neither belongs in a font registration, so it is written down in
pubspec.yamlandweb/index.htmlinstead of quietly left as a claim thatis nearly true.
Version: 2.23.0, covering this and the markdown emphasis work in #102,
which merged without a bump and so is on
mainbut was never published.build_versionis a dev dependency, not a runtime one. The reasonpackage_info_pluswas declined still holds — nothing reads the stampback, and the app should not carry a plugin to learn its own version. This
produces a checked-in constant, which is how every other generated file
here works. The guard test survives, re-aimed at catching a stale
app_version.dart, and I confirmed it fails when handed one rather thanassuming it still bites.
Two workarounds stay workarounds, and are now written down in
.claude/skills/run-app/rather than rediscovered:RangeError: Incorrect locale information providedfrom the JSIntl.Localeconstructor when a headless browserreports
navigator.language = "en-US@posix"(or"C"). It white-screensbefore
main()finishes. This is upstream —LocalizationService._resolvealready uses
basicLocaleListResolution, which never throws — and realbrowsers always send a valid tag, so it is documented rather than coded
around.
usePathUrlStrategy()means a refresh lands on/vault, which a plainstatic server 404s. Pages handles it via
web/404.html; locallytool/serve_web.mjsnow does the same.One policy change worth a look:
.gitignorehad a blanket.claude/,reasoned as "machine/environment-specific, not portable". A project skill
is the opposite of that, so it is narrowed to
.claude/*plus!.claude/skills/. Local config stays ignored — I checkedsettings.local.jsonstill is. Say if you would rather this lived indocs/alongsideprinting-fork.mdand I will move it.Generated by Claude Code