Skip to content

Serve CanvasKit and Roboto from the app, and make a release one edit - #103

Merged
IanHeinrich merged 1 commit into
mainfrom
claude/cv-template-work-auth-wqppca
Aug 26, 2026
Merged

Serve CanvasKit and Roboto from the app, and make a release one edit#103
IanHeinrich merged 1 commit into
mainfrom
claude/cv-template-work-auth-wqppca

Conversation

@IanHeinrich

Copy link
Copy Markdown
Owner

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 from fonts.gstatic.com. Neither is a choice anyone
made 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.html
already sets dartPdfJsBaseUrl to a local path for exactly that reason;
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. That is the check, not an inference.

A release is one edit again. BackupService._appVersion was a second
literal of pubspec's version:, kept in step by a test. That catches
drift, but does nothing about the failure that actually happened twice this
week: a merge with no bump at all, so deploy.yml skipped and the work
never shipped. It now reads a constant build_version generates from
pubspec.

How it was verified

  • dart format --output=none --set-exit-if-changed .
  • flutter analyze
  • flutter test --exclude-tags=golden — and the full run, 889
    passing
    including goldens
  • Checked in the browser

On the browser box: the app was driven headlessly with a Playwright route
handler that aborts and records any request not to localhost. Boot
and first render report EXTERNAL REQUESTS: NONE and no page errors, with
text rendering correctly — so the font really is coming from the bundle.
Repeated after merging main in, with the same result.

Goldens

  • Baselines regenerated and committed here

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 the
goldens, 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 new
baselines 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 Golden job is what actually adjudicates
them, 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 RegionPreset flag still pulls Noto Color Emoji, because
the 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.yaml and web/index.html instead of quietly left as a claim that
is nearly true.

Version: 2.23.0, covering this and the markdown emphasis work in #102,
which merged without a bump and so is on main but was never published.

build_version is a dev dependency, not a runtime one. The reason
package_info_plus was declined still holds — nothing reads the stamp
back, 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 than
assuming it still bites.

Two workarounds stay workarounds, and are now written down in
.claude/skills/run-app/ rather than rediscovered:

  • Flutter's engine throws RangeError: Incorrect locale information provided from the JS Intl.Locale constructor when a headless browser
    reports navigator.language = "en-US@posix" (or "C"). It white-screens
    before main() finishes. This is upstream — LocalizationService._resolve
    already uses basicLocaleListResolution, which never throws — and real
    browsers always send a valid tag, so it is documented rather than coded
    around.
  • usePathUrlStrategy() means a refresh lands on /vault, which a plain
    static server 404s. Pages handles it via web/404.html; locally
    tool/serve_web.mjs now does the same.

One policy change worth a look: .gitignore had 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 checked
settings.local.json still is. Say if you would rather this lived in
docs/ alongside printing-fork.md and I will move it.


Generated by Claude Code

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
@IanHeinrich
IanHeinrich merged commit 4b93b24 into main Aug 26, 2026
3 checks passed
@IanHeinrich
IanHeinrich deleted the claude/cv-template-work-auth-wqppca branch August 26, 2026 10:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants