feat: prerender suburb narrative into the page body - #166
Conversation
Previously prerender only swapped <head>, so a reviewer viewing page source (and any non-JS crawler) saw an empty <div id="root"> — the shell that reads as Low value content (adsense-plan §8.5.6 step 7). Prerender now seeds each suburb page's #root with a real data-driven paragraph. It transpiles src/lib/narrative.ts on the fly via the installed typescript dep, so the same buildNarrative() the SuburbNarrative component uses is the single source of truth. Tenure is fetched per suburb through a concurrency-limited pool (a failed suburb degrades to a head-only page, never aborts the build). The seed goes inside #root: main.tsx mounts with createRoot(), which replaces #root on load, so real users get the full SPA and the prose is only ever seen pre-hydration or by crawlers — no duplicate content.
📝 WalkthroughWalkthroughThe prerender script now fetches suburb tenure data, builds shared narratives, escapes the output, and seeds it into generated HTML. It limits concurrency, tolerates individual failures, cleans temporary files, and reports narrative coverage. ChangesPrerendered narratives
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant PrerenderScript
participant TenureAPI
participant buildNarrative
participant GeneratedHTML
PrerenderScript->>TenureAPI: Fetch suburb tenure data
TenureAPI-->>PrerenderScript: Return tenure data or null
PrerenderScript->>buildNarrative: Build narrative sections
buildNarrative-->>PrerenderScript: Return narrative content
PrerenderScript->>GeneratedHTML: Inject escaped content into `#root`
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
frontend/scripts/prerender.mjs (1)
140-146: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse Nocturne tokens for the seeded narrative.
Lines 142-146 hard-code colors, spacing, and font stacks. This can make the no-JavaScript page diverge from
frontend/src/components/SuburbNarrative.tsx.Use the existing Tailwind Nocturne classes for the generated section, paragraphs, and source line. Alternatively, add a static selector that uses Nocturne design tokens.
As per path instructions, use “Tailwind v4 with the Nocturne design tokens.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/scripts/prerender.mjs` around lines 140 - 146, Update the seeded narrative markup in the prerender function around the paragraphs map and returned section to use the existing Tailwind v4 Nocturne classes or a static selector backed by Nocturne design tokens, matching SuburbNarrative.tsx for the section, paragraphs, and source line. Remove the hard-coded colors, spacing, and font stack while preserving the escaped narrative content and structure.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/scripts/prerender.mjs`:
- Around line 120-127: Update the tenure requests in the mapLimit callback to
pass an AbortSignal.timeout() signal to fetch, with the timeout applied per
suburb request. Keep the existing try/catch fallback so aborted or failed
requests return null and prerendering continues.
---
Nitpick comments:
In `@frontend/scripts/prerender.mjs`:
- Around line 140-146: Update the seeded narrative markup in the prerender
function around the paragraphs map and returned section to use the existing
Tailwind v4 Nocturne classes or a static selector backed by Nocturne design
tokens, matching SuburbNarrative.tsx for the section, paragraphs, and source
line. Remove the hard-coded colors, spacing, and font stack while preserving the
escaped narrative content and structure.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 9a2203a4-df20-41a9-b84a-9c9973458149
📒 Files selected for processing (1)
frontend/scripts/prerender.mjs
| const tenures = await mapLimit(suburbs, NARRATIVE_CONCURRENCY, async ({ salCode }) => { | ||
| try { | ||
| const r = await fetch(`${API}/api/suburbs/${salCode}/tenure`) | ||
| if (!r.ok) return null | ||
| return await r.json() | ||
| } catch { | ||
| return null | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'Files matching prerender.mjs:\n'
fd -a 'prerender\.mjs$' . || true
file="$(fd 'prerender\.mjs$' . | head -n 1 || true)"
if [ -n "${file:-}" ]; then
printf '\nLine count:\n'
wc -l "$file"
printf '\nRelevant lines 90-150:\n'
sed -n '90,150p' "$file" | nl -ba -v90
printf '\nPackage/runtime declarations:\n'
for p in package.json frontend/package.json; do
if [ -f "$p" ]; then
echo "--- $p"
cat "$p" | sed -n '1,220p'
fi
done
fi
printf '\nSearch for fetch timeout / AbortSignal patterns in frontend scripts:\n'
rg -n "fetch\\(|AbortSignal|abort|timeout|Signal|mapLimit" frontend scripts . 2>/dev/null | head -n 200Repository: fatblaine/suburblens
Length of output: 353
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file="./frontend/scripts/prerender.mjs"
printf 'Relevant lines 90-150:\n'
awk 'NR>=90 && NR<=150 { printf "%4d %s\n", NR, $0 }' "$file"
printf '\nPackage/runtime declarations:\n'
for p in package.json frontend/package.json; do
if [ -f "$p" ]; then
echo "--- $p"
sed -n '1,240p' "$p"
fi
done
printf '\nRuntime availability for AbortSignal abort timeout:\n'
node - <<'JS'
console.log(process.version);
if (typeof AbortSignal === 'function' && AbortSignal.abort) {
console.log('AbortSignal.abort is available');
const controller = new AbortController();
const s = AbortSignal.timeout(10);
console.log(typeof s.timeout);
s.addEventListener('abort', () => controller.abort());
} else {
console.log('AbortSignal.abort is NOT available');
}
JS
printf '\nSearch for fetch timeout / AbortSignal patterns:\n'
rg -n "fetch\\(|AbortSignal|abort|timeout|Signal|mapLimit" frontend/scripts frontend/package.json package.json 2>/dev/null | head -n 200 || trueRepository: fatblaine/suburblens
Length of output: 5056
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file="./frontend/scripts/prerender.mjs"
printf 'Relevant implementation lines 1-90:\n'
awk 'NR>=1 && NR<=90 { printf "%4d %s\n", NR, $0 }' "$file"
python3 - <<'PY'
from pathlib import Path
import re
p = Path('./frontend/scripts/prerender.mjs')
src = p.read_text()
print('\nConcurrent fetch/await pattern in tenure mapLimit body:')
m = re.search(r'the single richest call.*?})\s*\)', src, re.S|re.M)
print('contains await fetch:', 'await fetch(`${API}/api/suburbs/${salCode}/tenure`)' in src)
print('contains catch return null:', 'return null\n }' in src[sr := src.find('async ({ salCode }) => {'):])
PYRepository: fatblaine/suburblens
Length of output: 5033
Add a timeout to each tenure request.
mapLimit starts one worker per concurrency slot, and each worker awaits fetch() before try/catch can run. If ten tenure requests stay pending, prerendering waits indefinitely. Use AbortSignal.timeout() around each fetch so a timed-out suburb still falls back to null and renders a head-only page.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/scripts/prerender.mjs` around lines 120 - 127, Update the tenure
requests in the mapLimit callback to pass an AbortSignal.timeout() signal to
fetch, with the timeout applied per suburb request. Keep the existing try/catch
fallback so aborted or failed requests return null and prerendering continues.
What
Seed each prerendered suburb page's
<body>with a real data-driven paragraph, instead of only swapping<head>. Single file changed:frontend/scripts/prerender.mjs.src/lib/narrative.tson the fly (via the installedtypescriptdep) and reuses itsbuildNarrative()— the exact function theSuburbNarrativecomponent uses, so there's one source of truth./tenureper suburb through a hand-rolled concurrency pool (limit 10); a failed suburb degrades to a head-only page rather than aborting the build.<section>with the paragraph +Source:line inside<div id="root">.Why
main.tsxmounts withcreateRoot(), so before this the raw HTML of every suburb page was an empty<div id="root">. An AdSense reviewer viewing "page source" (and any crawler that doesn't run JS) saw a shell — the "first-glance empty" that reads asLow value content. Now the value is in the served HTML itself.Because
createRoot()replaces#rooton load (it's nothydrateRoot), real users get the full SPA and the seeded prose is oever seen pre-hydration / by non-JS clients — no dup logic is transpiled from the shared.tsratherthan duplicated, and rather than relying on the buil strip types.Scope: the body seed is tenure-only (paragraph 1) to keep the build at +1494 requests, not ~+7500; the on-page card still renders all
three paragraphs. Static pages (
/about, `/methodolhead-only bodies — serialising their React content is a separate follow-up.How to test
cd frontend && npm run build— passes clean.VITE_API_BASE_URL=<api> npm run build:deploy— the final log readswrote N suburb pages (M with a seeded narrative).grep -o '<div id="root">.*</section>' dist/suburble already contains the paragraph text and sourceline inside#root`.npm run preview, open a suburb page — the SPA renders the full card with no duplicate static paragraph (React replaced the seed).curl -s https://www.suburblensapp.com/suburb/<code> | grep -A3 'id="root"'shows the prose; or DevTools → Disable JavaScript → reload a suburb page and confirm the paSummary by CodeRabbit
New Features
Performance
Improvements