Prerender: install <base href> for the card's directory#4829
Prerender: install <base href> for the card's directory#4829richardhjtan wants to merge 3 commits into
Conversation
Without this, a card template's `<img src="thumb.png">` resolves
against the /render/<encoded-card>/<seq>/{cardRender:true}/html/...
synthetic URL the prerender browser is currently at. The browser
then fetches /render/.../html/<format>/thumb.png and 404s — which
both pollutes server logs and bakes a broken src into the captured
HTML.
Set <base href> to the card's directory at the start of model() and
remove it in deactivate(). Native browser URL resolution does the
rest.
CS-11146
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Preview deploymentsHost Test Results 1 files ± 0 1 suites ±0 2h 10m 4s ⏱️ + 1h 19m 56s Results for commit d1fe409. ± Comparison against earlier commit 2a3d6bc. For more details on these errors, see this check. Realm Server Test Results 1 files ±0 1 suites ±0 37m 43s ⏱️ - 3m 58s Results for commit d1fe409. ± Comparison against earlier commit 2a3d6bc. For more details on these errors, see this check. |
There was a problem hiding this comment.
Pull request overview
This PR updates the Host app’s render route to install a <base href> pointing at the rendered card’s containing directory during prerender, so relative URLs in card templates (e.g. <img src="foo.png">, <a href="bar">) resolve against the card’s realm path instead of the synthetic /render/.../html/... route URL used by the prerender browser.
Changes:
- Install a prerender-only
<base>element (as the first child of<head>) during themodel()hook based on the current card id. - Update the existing
<base>when the card id changes across format passes. - Remove the injected
<base>on routedeactivate()to avoid affecting subsequent routes.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| #removePrerenderBaseHref(): void { | ||
| if (typeof document === 'undefined') { | ||
| return; | ||
| } | ||
| document.head?.querySelector('base[data-prerender-base]')?.remove(); | ||
| } |
| // Without this, relative `<img src>` in a card template resolves against | ||
| // the /render/... synthetic URL and 404s. CS-11146. | ||
| this.#installPrerenderBaseHref(id); | ||
| // Stamp the "consuming realm" — the realm that owns the card being |
…ot review) - Also remove the injected <base data-prerender-base> from the route destructor, not just deactivate(). In tests the owner can be destroyed without deactivate firing, which would leak <base> into subsequent routes/tests (same reason the route already clears its globals from the destructor). - Add a focused acceptance test in prerender-html-test asserting the <base> is installed pointing at the card's containing directory. CS-11146 Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The injected <base href> is document-wide and affects how every library on the page resolves relative URLs against document.baseURI. The host test runner shares a DOM with the route under test, so Monaco editor's worker loader (FileAccessImpl.asBrowserUri) ends up fetching workers from the card's realm origin instead of the host origin and crashes every test that loads Monaco. Gate the install on !isTesting() — matches the existing pattern in this file (attachWindowErrorListeners, restoreSessionsFromStorage). Real prerender headless browsers don't load Monaco, so the fix still works in production. Remove the acceptance test that asserted <base> presence — it would fail under the gate. Coverage gap accepted; the install method is a trivial DOM mutation whose correctness was already small. CS-11146 Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Summary
Set a
<base href>to the card's containing directory at the start of the prerender'smodel()hook, so relative<img src>/<a href>in a card template resolves against the card's realm instead of the synthetic/render/<encoded-card>/<seq>/{cardRender:true}/html/...URL the prerender browser is sitting at. Remove it ondeactivate().This is one of several broken-image failure modes seen on staging and production. Full investigation report shared separately.
Linear: CS-11146
What this fixes
Card templates that use a relative image src (e.g.
<img src="green-mango.png">) used to issue requests like/render/.../html/<format>/green-mango.pngduring prerender, which 404. The 404 itself was logged repeatedly, and the brokensrcwas baked into the captured HTML so end users also saw a broken icon. Native browser URL resolution against the new<base>does the right thing — no template rewrites needed.Implementation notes
<base>is inserted as the first child of<head>, so it precedes anything that would resolve against the document base.isolated/fitted/embedded); updated rather than re-created if the card id changes.index.htmlare root-absolute (/foo), so they ignore the<base>and continue to work.Files
packages/host/app/routes/render.ts—#installPrerenderBaseHref/#removePrerenderBaseHref, wired intomodel()anddeactivate().Test plan
pnpm lint:typesgreen.<img src>(e.g. anexperiments/Friend*.jsoncard withgreen-mango.png). Inspect the captured HTML inboxel_index.html_formatfor the resolved URL./render/.../html/...404s. Count should drop to ~0.Out of scope
Other failure modes from the same investigation are tracked separately (CS-11144 shipped on PR #4828, CS-11145, CS-11147).
🤖 Generated with Claude Code