Skip to content

feat: pre-render site to static HTML at build time (SSG)#42

Closed
andimrob wants to merge 154 commits intomainfrom
rob/use-server-side-render
Closed

feat: pre-render site to static HTML at build time (SSG)#42
andimrob wants to merge 154 commits intomainfrom
rob/use-server-side-render

Conversation

@andimrob
Copy link
Owner

@andimrob andimrob commented Feb 12, 2026

Summary

  • Add a post-build prerender step that generates fully-formed HTML using renderToString, so crawlers see real content instead of an empty <div id="root"></div>
  • Switch client entry from createRoot to hydrateRoot so the browser hydrates the pre-rendered markup into an interactive SPA
  • Prevent dark-to-light FOUC with an inline theme detection script that runs before first paint

Changes

New files

  • src/entry-server.tsx — SSR render function exporting render() via renderToString
  • scripts/prerender.js — post-build script using Vite's ssrLoadModule to render the app and inject HTML into the build output

Modified files

  • src/main.tsxcreateRoot().render()hydrateRoot()
  • src/hooks/useTheme.ts — guard useState initializer for SSR (typeof window === 'undefined'"dark")
  • src/hooks/useTypewriter.ts — render full text on server for SEO, empty string on client for animation
  • src/components/Hero.tsxsuppressHydrationWarning on typewriter span (SSR has full text, client starts empty)
  • index.html — add dark class default + inline FOUC-prevention script
  • package.json — build script adds && node scripts/prerender.js
  • eslint.config.js — add scripts/ to ignores (Node.js build tooling)

Test plan

  • pnpm run build — tsc + vite build + prerender all pass
  • pnpm run typecheck — passes
  • pnpm vitest run — 3 tests pass
  • pnpm run lint — 0 errors
  • pnpm run format:check — all files formatted
  • Inspected public/index.html<div id="root"> contains rendered HTML ("Robert Blakey", nav, sections)
  • pnpm run preview — visual check that hydration works, typewriter animates, theme toggle works

Closes #39

🤖 Generated with Claude Code


Summary by cubic

Pre-render the site to static HTML at build so crawlers see real content, then hydrate on the client for an interactive SPA. Also prevents dark-to-light FOUC on first paint with a before-paint theme script.

  • New Features

    • Post-build prerender using Vite ssrLoadModule to inject rendered HTML into public/index.html.
    • New SSR entry point exporting render() with renderToString.
    • Switched client bootstrap from createRoot to hydrateRoot.
  • Bug Fixes

    • Made theme and typewriter hooks SSR-safe (server defaults, full text for SEO).
    • index.html sets a default dark class and runs a pre-paint script that reads localStorage or prefers-color-scheme to toggle the theme (no FOUC).

Written for commit a897e35. Summary will update on new commits.

andimrob and others added 26 commits February 10, 2026 22:35
Sync quip background colors across all three prism faces (top, bottom,
front underline) using a parallel quipBgs array and faceBg state that
updates on rotate-back. Fix hero typewriter animation on mobile by
reserving full name width upfront and positioning cursor absolutely
to prevent line-break jank.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Break up the 525-line Header into quip data, magnetic tilt hook,
prism flip hook, and a thin orchestration shell. Use semantic
<header> and <nav> elements.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Neither hook is imported anywhere — deleting to reduce clutter.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Expose getMousePosition() instead of live-bound let exports to
prevent silent breakage from destructured imports.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Caveat was loaded but never referenced — removes an unnecessary
network request on every page load.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Vite serves static/ as root, so the runtime path should be
/fonts/SimpleCakes.ttf, not /static/fonts/SimpleCakes.ttf.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Install and configure ESLint 9 (flat config) with typescript-eslint,
react-hooks, react-refresh, and eslint-config-prettier. Add Prettier
with project-matching style. Install Storybook 10 with react-vite
framework and addon suite. Add typecheck, lint, format, and storybook
scripts. Remove package-lock.json to standardize on pnpm (resolves #27).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move ref assignment in useActiveSection into useEffect to satisfy
react-hooks/refs rule. Remove unnecessary regex escape in xray.ts.
Apply Prettier formatting to Hero.tsx, confetti.ts, and main.tsx.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace npm ci with pnpm install --frozen-lockfile using
pnpm/action-setup@v4. Add format:check, lint, and typecheck
steps before the build step.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Configure Storybook with @tailwindcss/vite plugin via viteFinal so
Tailwind classes render in stories. Add dark mode toolbar toggle via
decorator that toggles .dark class on <html>. Create Header stories:
Default, WithScrollSections (mock scroll targets), and DarkMode.
Include vitest addon integration for story-based testing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The component was a 9-line wrapper around a single <span>. Inline
the element directly and delete CursiveRob.tsx.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace 9 standalone gradient classes and keyframes (~207 lines) with
a shared base rule and CSS custom properties (~68 lines). Each variant
now only sets --prism-stops, --prism-size, --prism-speed, and
--prism-timing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Guard useState initializers in useTheme and useTypewriter to return
safe defaults when window is undefined (SSR). Add suppressHydrationWarning
to Hero's typewriter span since server renders full text while client
starts empty for animation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add entry-server.tsx that exports a render() function using
renderToString for static HTML generation. Switch main.tsx from
createRoot to hydrateRoot so the client hydrates pre-rendered markup.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add scripts/prerender.js that uses Vite's ssrLoadModule to render the
app to static HTML and inject it into the build output. Update build
script to run prerender after vite build. Add scripts/ to eslint ignores.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add dark class to <html> as default and inject an inline script that
reads localStorage/matchMedia before first paint to set the correct
theme class, preventing a flash of unstyled content.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@netlify
Copy link

netlify bot commented Feb 12, 2026

Deploy Preview for symphonious-blancmange-7aafce ready!

Name Link
🔨 Latest commit a897e35
🔍 Latest deploy log https://app.netlify.com/projects/symphonious-blancmange-7aafce/deploys/699f5603b654c60008fb25a6
😎 Deploy Preview https://deploy-preview-42--symphonious-blancmange-7aafce.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

No issues found across 9 files

@andimrob andimrob closed this Feb 25, 2026
@andimrob andimrob force-pushed the rob/use-server-side-render branch from d4b27c5 to a897e35 Compare February 25, 2026 20:05
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.

Pre-render site to static HTML at build time (SSG)

1 participant