Skip to content

Prerender: install <base href> for the card's directory#4829

Draft
richardhjtan wants to merge 3 commits into
mainfrom
CS-11146-prerender-base-href
Draft

Prerender: install <base href> for the card's directory#4829
richardhjtan wants to merge 3 commits into
mainfrom
CS-11146-prerender-base-href

Conversation

@richardhjtan
Copy link
Copy Markdown
Contributor

Summary

Set a <base href> to the card's containing directory at the start of the prerender's model() 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 on deactivate().

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.png during prerender, which 404. The 404 itself was logged repeatedly, and the broken src was 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.
  • Reused across format passes for the same card (isolated / fitted / embedded); updated rather than re-created if the card id changes.
  • Removed on route deactivate so subsequent non-render routes are not affected.
  • Same-origin app assets in the host's index.html are root-absolute (/foo), so they ignore the <base> and continue to work.

Files

  • packages/host/app/routes/render.ts#installPrerenderBaseHref / #removePrerenderBaseHref, wired into model() and deactivate().

Test plan

  • pnpm lint:types green.
  • Manual repro on staging: trigger a re-index of a card that uses a relative <img src> (e.g. an experiments/Friend*.json card with green-mango.png). Inspect the captured HTML in boxel_index.html_format for the resolved URL.
  • 7 days post-deploy, re-run the log scan for /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

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]>
@richardhjtan richardhjtan requested a review from Copilot May 14, 2026 12:35
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 14, 2026

Preview deployments

Host Test Results

    1 files  ±    0      1 suites  ±0   2h 10m 4s ⏱️ + 1h 19m 56s
2 294 tests +1 426  2 229 ✅ +1 465  15 💤 +7   2 ❌  - 12  48 🔥  - 34 
2 313 runs  +1 426  2 200 ✅ +1 499  15 💤 +7  50 ❌  - 46  48 🔥  - 34 

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
1 365 tests ±0  1 167 ✅  - 1  0 💤 ±0  198 ❌ +1 
1 444 runs  ±0  1 231 ✅  - 1  0 💤 ±0  213 ❌ +1 

Results for commit d1fe409. ± Comparison against earlier commit 2a3d6bc.

For more details on these errors, see this check.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 the model() hook based on the current card id.
  • Update the existing <base> when the card id changes across format passes.
  • Remove the injected <base> on route deactivate() to avoid affecting subsequent routes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1549 to +1554
#removePrerenderBaseHref(): void {
if (typeof document === 'undefined') {
return;
}
document.head?.querySelector('base[data-prerender-base]')?.remove();
}
Comment thread packages/host/app/routes/render.ts Outdated
Comment on lines 218 to 221
// 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]>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

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]>
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