Skip to content

feat: prerender suburb narrative into the page body - #166

Merged
fatblaine merged 1 commit into
devfrom
feat/adsense
Aug 6, 2026
Merged

fatblaine merged 1 commit into
devfrom
feat/adsense

Conversation

@fatblaine

@fatblaine fatblaine commented Aug 6, 2026 •

Copy link
Copy Markdown
Owner

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.

  • Transpiles src/lib/narrative.ts on the fly (via the installed typescript dep) and reuses its buildNarrative() — the exact function the SuburbNarrative component uses, so there's one source of truth.
  • Fetches /tenure per suburb through a hand-rolled concurrency pool (limit 10); a failed suburb degrades to a head-only page rather than aborting the build.
  • Injects a <section> with the paragraph + Source: line inside <div id="root">.

Why

main.tsx mounts with createRoot(), 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 as Low value content. Now the value is in the served HTML itself.
Because createRoot() replaces #root on load (it's not hydrateRoot), 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 .ts ratherthan 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 reads wrote 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).
  • After deploy: 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 pa

Summary by CodeRabbit

  • New Features

    • Suburb pages now include data-driven narrative content during initial page loading.
    • Pages remain available even when individual tenure data cannot be retrieved.
  • Performance

    • Tenure data requests are processed with controlled concurrency to improve build reliability.
  • Improvements

    • Enhanced progress reporting and final coverage statistics for generated narratives.
    • Improved HTML safety and temporary-file handling during page generation.

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.
@fatblaine
fatblaine requested a lite review from Copilot August 6, 2026 07:06
@fatblaine fatblaine self-assigned this Aug 6, 2026
@fatblaine fatblaine added the enhancement New feature or request label Aug 6, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The 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.

Changes

Prerendered narratives

Layer / File(s) Summary
Load narrative and fetch suburb data
frontend/scripts/prerender.mjs
The script transpiles and loads buildNarrative. It fetches tenure data with concurrency limited to 10 and tolerates individual failures.
Complete page generation and report coverage
frontend/scripts/prerender.mjs
Generated pages receive escaped narrative content in #root. The script updates completion logging with seeded-page counts and preserves sitemap generation.

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`
Loading

Possibly related PRs

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: prerendering suburb narrative content into the page body.
Description check ✅ Passed The description includes the required What, Why, and How to test sections with relevant implementation, motivation, scope, and validation details.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/adsense

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@fatblaine
fatblaine merged commit de189c3 into dev Aug 6, 2026
4 of 5 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
frontend/scripts/prerender.mjs (1)

140-146: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use 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

📥 Commits

Reviewing files that changed from the base of the PR and between c287ac9 and 5fb8f63.

📒 Files selected for processing (1)
  • frontend/scripts/prerender.mjs

Comment on lines +120 to +127
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
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 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 200

Repository: 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 || true

Repository: 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 }) => {'):])
PY

Repository: 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants