Skip to content

fix(web): cache-busted link-preview image URLs - #33

Merged
vlidholt merged 2 commits into
serverpod:mainfrom
vlidholt:fix/link-preview-cache-busting
Oct 22, 2026
Merged

vlidholt merged 2 commits into
serverpod:mainfrom
vlidholt:fix/link-preview-cache-busting

Conversation

@vlidholt

Copy link
Copy Markdown
Contributor

Why

Link previews on X and LinkedIn sometimes show the wrong image. Production serves the right tags and a valid image right now (checked as Twitterbot and LinkedInBot), so the likely cause is the crawlers' caches:

  • They cache each shared page's preview for about 7 days. The preview tags only shipped on Oct 17 (feat(web): Link previews with the Wonky Work image #2). URLs scraped before that, or during a deploy, keep their old preview (LinkedIn picks another image from the page when there's no og:image).
  • The image URL had no content hash, while /web/ is served as immutable, max-age=31536000 by StaticRoute.withCacheBusting. The CDN and both crawlers cache the image by URL, so a changed og-1.jpg would never be refetched. The route had cache busting enabled, but no page linked the hashed URLs.

What

Pages now link /web/ assets by their content hash (/web/images/og-1@<hash>.jpg) through Serverpod's built-in template cache busting: {{{@/web/…}}} in a WidgetRoute with cacheBustingConfig.

  • App shells are templates. Each app's web/index.html says https://wonky.work{{{@/web/images/og-1.jpg}}}. The new FlutterAppRoute is Serverpod's FlutterRoute (build files, SPA fallback, WASM headers), except that /<app>/, /<app>/index.html and the SPA fallback are served by AppShellRoute. That route parses the build's index.html once, and again when the file changes. The build's own files stay without cache busting, which would strip @… from asset names like icon@2x.png. whiskers, the template engine Serverpod uses, is now a direct dependency of the server.
  • Legal pages move from web/pages/ to web/templates/ and are served by LegalPageRoute. Their favicon is cache-busted too.
  • specs/01 §2.4 describes the routes, the trade-off and how to refresh crawler caches (LinkedIn Post Inspector).

Trade-off: WidgetRoute answers private, no-cache without an ETag, so browsers re-download the ~5 KB shell instead of getting a 304.

After the deploy, the image gets a new URL, so crawlers and the CDN fetch it fresh. Page previews they already cached stay until they expire.

Testing

  • dart analyze --fatal-infos is clean.
  • New tests pass: test/integration/web/flutter_app_route_test.dart (the three shell paths, HEAD, WASM headers, build files, icon@2x.png) and test/unit/web/legal_page_route_test.dart (both pages, HEAD, the hashed image served as immutable JPEG).
  • A real flutter build web --wasm of Drive keeps the placeholder in the built index.html.
  • Not yet checked on a dev server with the new routes, which needs a restart.

🤖 Generated with Claude Code

vlidholt and others added 2 commits October 22, 2026 15:53
The app shells and the legal pages linked the Open Graph image as
/web/images/og-1.jpg, which /web/ (StaticRoute.withCacheBusting) serves as
immutable for a year: a changed image would stay stale in the CDN and in
the X and LinkedIn preview caches. Pages now link it by its content hash,
/web/images/og-1@<hash>.jpg, through Serverpod's built-in template cache
busting ({{{@/web/...}}} in a WidgetRoute with cacheBustingConfig):

- The app shells are templates: FlutterAppRoute is FlutterRoute with
  /<app>/, /<app>/index.html and the SPA fallback served by AppShellRoute,
  which parses the build's index.html once (again when it changes). The
  build's own files stay without cache busting (icon@2x.png).
- The legal pages move to web/templates and are served by LegalPageRoute.
- specs/01 §2.4 describes the routes and the crawler caches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The app shell templates (AppShellRoute) parse the build's index.html with
whiskers, Serverpod's template engine: Serverpod's template registry only
loads web/templates. Listed in specs/01 §2.1 and the dependency-rule check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vlidholt
vlidholt merged commit 89097e1 into serverpod:main Oct 22, 2026
41 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 22, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant