Skip to content

feat: prerender 624 multilingual blog articles + recharts chunk split#20

Merged
miquelmatoses merged 1 commit into
mainfrom
feat/prerender-blog-slugs-multilingual
May 16, 2026
Merged

feat: prerender 624 multilingual blog articles + recharts chunk split#20
miquelmatoses merged 1 commit into
mainfrom
feat/prerender-blog-slugs-multilingual

Conversation

@miquelmatoses
Copy link
Copy Markdown
Collaborator

Summary

  • Blog pre-rendering: scripts/prerender.mjs now generates a static dist/blog/<slug>/index.html (and dist/<lang>/blog/<slug>/index.html for CA/ES/FR/DE/DA) for all 104 blog slugs, so GitHub Pages serves HTTP 200 instead of the 404 SPA-redirect that Googlebot was seeing. A concurrency pool of 4 parallel Chrome tabs keeps CI build time ~3–4 min instead of 50+ min for sequential execution.
  • Sitemap re-enabled: generate-sitemap.mjs un-comments the blog slug block (disabled in Phase 15 because of 404s). Sitemap grows from 8 → 112 <loc> entries, all with full hreflang alternates.
  • recharts chunk split: vite.config.js isolates recharts + its d3/victory/internmap deps into a dedicated vendor-recharts chunk (74 KiB gz). The vendor chunk drops from 138 → 62 KiB gz. recharts is never on the critical path for / (all consumers are lazy-loaded report/role pages).

Verification

  • probe-meta.mjs (temporary, deleted) confirmed puppeteer captures article-specific <title>, <meta description>, and <link rel="canonical"> at networkidle0 + 1500ms — no waitForFunction() guard needed.
  • Build: ✅ clean, no warnings
  • Bundle sanity (all 6 brand colors in JS chunks): ✅
  • 194 frontend tests: ✅

Test plan

  • CI passes (build + bundle sanity + frontend tests + backend tests)
  • After merge + deploy, verify curl -s -o /dev/null -w "%{http_code}" https://cercol.team/blog/big-five-personality-across-cultures-what-research-shows returns 200
  • Verify same for a non-English slug, e.g. https://cercol.team/ca/blog/big-five-personality-across-cultures-what-research-shows
  • Verify https://cercol.team/sitemap.xml contains blog article URLs
  • Confirm vendor-recharts-*.js present in dist/assets/ and not loaded on homepage network tab

🤖 Generated with Claude Code

- prerender.mjs: concurrency pool (4 parallel Chrome tabs) renders all
  104 blog slugs × 6 languages = 624 routes, producing static
  dist/blog/<slug>/index.html and dist/<lang>/blog/<slug>/index.html so
  GitHub Pages serves HTTP 200 instead of the 404-redirect for crawlers
- generate-sitemap.mjs: re-enable blog slug URLs now that static files
  exist; sitemap grows from 8 to 112 <loc> entries with full hreflang
- vite.config.js: isolate recharts + d3/victory deps into vendor-recharts
  chunk (74 KiB gz); vendor chunk drops from 138 → 62 KiB gz; recharts
  is never on the critical path (all consumers are lazy-loaded)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@miquelmatoses miquelmatoses merged commit 9d96fe6 into main May 16, 2026
3 checks passed
@miquelmatoses miquelmatoses deleted the feat/prerender-blog-slugs-multilingual branch May 16, 2026 21:05
miquelmatoses added a commit that referenced this pull request May 16, 2026
…, optimize images (#21)

Production audit caught three remaining issues:

1. /blog pre-rendered HTML in production contains the 'Could not
   load articles' error fallback. Google Search Console rejects
   indexing with 'Soft 404'. Root cause: during the PR #20 CI build,
   BlogIndexPage's API call during prerender failed (likely due to
   the 4-page concurrent pool hammering api.cercol.team with too
   many simultaneous requests). Puppeteer captured the error state.
   Commit 131c986 (waitForFunction guard) was on main but never
   deployed (scripts/ missing from deploy path filter), AND even if
   deployed wouldn't help in this case — if the API stays failed
   during build, the guard times out and the error HTML is captured
   anyway.

2. 91 of 104 blog cover image URLs in the database have malformed
   double-? query strings, causing Unsplash to serve 3840px+ images
   at 380x214 display. The blog slug LCP is dominated by this:
   722 KiB cover image, LCP 9.1s.

3. BetaBanner causes 1300ms LCP delay on the landing page because
   useState(null) unmounts the pre-rendered banner during React
   hydration; the API roundtrip then re-mounts it 1300ms later.

The unified solution: stop depending on API calls at render time
for pre-rendered routes. The prerender script fetches the article
list and beta status ONCE and injects them as window globals into
every pre-rendered HTML. React reads from window synchronously,
eliminating the hydration flicker and the API failure mode.

Changes:

deploy-frontend.yml: add scripts/** to path filter so prerender
changes trigger fresh deploys.

src/utils/unsplash.js (new): normalizeUnsplashUrl() fixes the
double-? URLs and applies w/q/fit overrides. 5 unit tests.

BlogArticlePage.jsx: cover uses normalizeUnsplashUrl(coverUrl,
{w:760}) + fetchpriority='high' + loading='eager' + explicit
width/height.

BlogIndexPage.jsx:
- useState reads from window.__BLOG_ARTICLES__ on first render
- useEffect still refreshes from API for stale-data resilience
- Error state only shown when we have NO articles from ANY source
- Thumbnails use normalizeUnsplashUrl(coverUrl, {w:400}) + lazy

BetaBanner.jsx: useState reads from window.__BETA__ on first render.

scripts/prerender.mjs:
- fetchBlogArticles() called once at start (throws on failure to
  fail the build loudly rather than silently shipping broken HTML)
- fetchBetaStatus() called once at start (silent fallback to
  {remaining:500,total:500,active:true})
- Both values injected as <script>window.__BETA__=...;
  window.__BLOG_ARTICLES__=...;</script> in <head> of every HTML

Expected outcomes:
- /blog and 5 language variants: HTML contains 104 article cards,
  no 'Could not load' fallback. Google Search Console accepts
  indexing.
- Blog slug LCP: 9.1s -> ~2.5s
- Blog slug Performance: 66 -> ~85-90
- Landing LCP: 4.4s -> ~2.5-3.0s
- Landing Performance: 76 -> ~82-86

Co-authored-by: miquelmatoses <miquelmatoses@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
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.

1 participant