Prerender hoists body tags and replaces its markers unsafely #232

Closed
opened 2026-09-10 04:13:35 -05:00 by null · 1 comment
Owner

Seen at a25077d (v0.9.5).

What is true now, in scripts/prerender.js:

  • the hoist regex (lines 59-60) moves every <title>, <meta> and <link> out of the body, so an inline SVG <title> or a microdata <meta itemprop> in content becomes a second page title or a stray head tag;
  • page.replace('</head>', str) (lines 90-91) interprets $&, $' and $$ in the replacement, and React escapes & into entities, so a $ directly before one injects markup. Zero $ in today's copy; future pages may carry prices;
  • nothing asserts a replacement matched, and running prerender twice duplicates canonicals, because dist/index.html is both template and output;
  • a render error fails the build without naming the route, and a Suspense fallback ships silently, marked only by <!--$!-->.

What to do. Function replacers; assert each marker matches exactly once; refuse a template that already has a canonical; the hoist skips anything inside <svg> or carrying itemprop; rethrow render errors with the route; fail on <!--$!-->.

Verify: with a probe <svg><title> in the footer the build still has exactly one <title> per head, a thrown render error names its route, and a Suspense-wrapped page fails the build.

Seen at a25077d (v0.9.5). **What is true now,** in `scripts/prerender.js`: - the hoist regex (lines 59-60) moves every `<title>`, `<meta>` and `<link>` out of the body, so an inline SVG `<title>` or a microdata `<meta itemprop>` in content becomes a second page title or a stray head tag; - `page.replace('</head>', str)` (lines 90-91) interprets `$&`, `$'` and `$$` in the replacement, and React escapes `&` into entities, so a `$` directly before one injects markup. Zero `$` in today's copy; future pages may carry prices; - nothing asserts a replacement matched, and running prerender twice duplicates canonicals, because `dist/index.html` is both template and output; - a render error fails the build without naming the route, and a Suspense fallback ships silently, marked only by `<!--$!-->`. **What to do.** Function replacers; assert each marker matches exactly once; refuse a template that already has a canonical; the hoist skips anything inside `<svg>` or carrying `itemprop`; rethrow render errors with the route; fail on `<!--$!-->`. Verify: with a probe `<svg><title>` in the footer the build still has exactly one `<title>` per head, a thrown render error names its route, and a Suspense-wrapped page fails the build.
null added this to the Batch 18: Guards and landmines found building Batch 17 milestone 2026-09-10 04:13:35 -05:00
null added the
P2
bug
seo
labels 2026-09-10 04:13:35 -05:00
Author
Owner

Fixed in 28b07ab. The hoist now parks inline blocks first and leaves in place; both marker substitutions are a split and join instead of String.replace, so a dollar sign in copy cannot inject markup, and each marker must appear exactly once; render errors are rethrown naming the route; a Suspense fallback marker fails the build; and the script refuses a template that already carries a canonical. Proven by mutation: a probe

Fixed in 28b07ab. The hoist now parks inline <svg> blocks first and leaves <meta itemprop> in place; both marker substitutions are a split and join instead of String.replace, so a dollar sign in copy cannot inject markup, and each marker must appear exactly once; render errors are rethrown naming the route; a Suspense fallback marker fails the build; and the script refuses a template that already carries a canonical. Proven by mutation: a probe <svg><title> in the footer stayed in the body on all 19 pages with no page gaining a second title; copy reading 'Save $10 & more' reached /support literally with one root div; a Suspense boundary failed the build with 'prerender: / shipped a Suspense fallback'; a second prerender run refused with 'dist/index.html already carries a canonical'.
null closed this issue 2026-09-10 04:32:51 -05:00
Sign in to join this conversation.
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: null/Queue-North-Website#232
No description provided.