{"id":460,"date":"2026-06-01T14:57:32","date_gmt":"2026-06-01T14:57:32","guid":{"rendered":"https:\/\/saveyourclicks.com\/blog\/build-diaries\/building-3d-homepage-react-three-fiber-claude\/"},"modified":"2026-06-01T15:52:02","modified_gmt":"2026-06-01T15:52:02","slug":"building-3d-homepage-react-three-fiber-claude","status":"publish","type":"post","link":"https:\/\/saveyourclicks.com\/blog\/en\/build-diaries\/building-3d-homepage-react-three-fiber-claude\/","title":{"rendered":"Building an SEO-Friendly 3D Homepage with React Three Fiber and Claude"},"content":{"rendered":"\n<link rel=\"preconnect\" href=\"https:\/\/fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https:\/\/fonts.gstatic.com\" crossorigin>\n<link rel=\"stylesheet\" media=\"print\" onload=\"this.media='all';this.onload=null\" href=\"https:\/\/fonts.googleapis.com\/css2?family=Inter:wght@400;600;700;800&#038;display=swap\">\n\n<noscript><link rel=\"stylesheet\" href=\"https:\/\/fonts.googleapis.com\/css2?family=Inter:wght@400;600;700;800&#038;display=swap\"><\/noscript>\n\n<style>html{scroll-behavior:smooth;scroll-padding-top:32px}@media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}}.bd-article{--bg:#ffffff;--bg-alt:#f8fafc;--bg-dark:#0a0f1e;--primary:#3b82f6;--primary-dark:#2563eb;--accent:#8b5cf6;--accent-soft:#faf5ff;--gradient:linear-gradient(135deg,#3b82f6,#8b5cf6);--text:#0f172a;--text-2:#475569;--text-muted:#94a3b8;--text-light:#f1f5f9;--border:#e2e8f0;--border-strong:#cbd5e1;--good:#16a34a;--good-bg:#dcfce7;--warn:#dc2626;--warn-bg:#fee2e2;--warn-soft:#fef2f2;--code-bg:#f1f5f9;--radius:12px;--radius-lg:16px;--shadow-sm:0 1px 2px rgba(15,23,42,.04);--shadow:0 4px 12px rgba(15,23,42,.06);--shadow-lift:0 12px 32px rgba(15,23,42,.08);--ease:cubic-bezier(.22,1,.36,1);font-family:'Inter',-apple-system,BlinkMacSystemFont,sans-serif;color:var(--text);background:var(--bg);line-height:1.65;-webkit-font-smoothing:antialiased;font-weight:400}.bd-article *,.bd-article *::before,.bd-article *::after{box-sizing:border-box}.bd-article p{margin:0 0 1em}.bd-article p:last-child{margin-bottom:0}.bd-article a{color:var(--primary-dark);text-decoration:none;transition:color .15s var(--ease)}.bd-article a:hover{color:var(--primary)}.bd-article code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.92em;background:var(--code-bg);color:var(--text);padding:.1em .4em;border-radius:4px;border:1px solid var(--border)}.bd-article em{font-style:normal;color:var(--primary-dark);font-weight:600}.bd-container{max-width:840px;margin:0 auto;padding:0 24px}.bd-article .bd-hero{background:var(--bg-dark);color:var(--text-light);padding:88px 0 72px;position:relative;overflow:hidden}.bd-article .bd-hero::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse 70% 50% at 50% -10%,rgba(59,130,246,.18),transparent 60%),radial-gradient(ellipse 50% 40% at 90% 90%,rgba(139,92,246,.14),transparent 60%);pointer-events:none}.bd-article .bd-hero > .bd-container{position:relative;z-index:1}.bd-article .bd-eyebrow{display:inline-flex;align-items:center;gap:8px;padding:6px 14px;margin-bottom:28px;background:rgba(59,130,246,.1);border:1px solid rgba(59,130,246,.25);border-radius:50px;font-size:13px;font-weight:600;color:#93c5fd}.bd-article .bd-eyebrow time{color:var(--text-muted);font-weight:400}.bd-article .bd-dot{width:6px;height:6px;border-radius:50%;background:#22c55e;animation:bd-pulse 2s ease-in-out infinite}@keyframes bd-pulse{0%,100%{opacity:1}50%{opacity:.4}}.bd-article .bd-hero h1{font-size:clamp(32px,5.5vw,52px);font-weight:800;letter-spacing:-.025em;line-height:1.12;color:var(--text-light);margin:0 0 20px}.bd-article .bd-grad{background:var(--gradient);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:transparent}.bd-article .bd-lead{font-size:18px;color:var(--text-muted);line-height:1.7;max-width:680px;margin:0 0 40px}.bd-article .bd-honesty{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:20px;padding-top:32px;border-top:1px solid rgba(255,255,255,.08);margin:0}.bd-article .bd-honesty > div{display:flex;flex-direction:column-reverse;gap:4px;min-width:0}.bd-article .bd-honesty dt{font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em;margin:0;font-weight:600}.bd-article .bd-honesty dd{font-size:24px;font-weight:800;color:var(--text-light);margin:0;letter-spacing:-.02em;line-height:1.1}.bd-article .bd-timeline{padding:72px 0 32px;background:var(--bg)}<\/style>\n\n<link rel=\"stylesheet\" media=\"print\" onload=\"this.media='all';this.onload=null\" href=\"\/blog\/wp-content\/uploads\/build-diaries\/building-3d-homepage-react-three-fiber-claude.css?v=b8bb7505\">\n<noscript><link rel=\"stylesheet\" href=\"\/blog\/wp-content\/uploads\/build-diaries\/building-3d-homepage-react-three-fiber-claude.css?v=b8bb7505\"><\/noscript>\n\n<script type=\"application\/ld+json\">\n{\n  \"@context\": \"https:\/\/schema.org\",\n  \"@type\": \"Article\",\n  \"headline\": \"Building an SEO-Friendly 3D Homepage with React Three Fiber and Claude\",\n  \"description\": \"A timestamped build diary about rebuilding a homepage as a scroll-driven 3D website with React Three Fiber and Claude \u2014 then, the same day, the cutover: making a client-rendered WebGL page crawlable and indexable with build-time prerendering, an error boundary, and code-splitting that cut first-load JS by 83%.\",\n  \"author\": { \"@type\": \"Person\", \"name\": \"Yusof Ansari\", \"url\": \"https:\/\/saveyourclicks.com\/\" },\n  \"publisher\": { \"@type\": \"Organization\", \"name\": \"Save Your Clicks\", \"url\": \"https:\/\/saveyourclicks.com\/\", \"logo\": { \"@type\": \"ImageObject\", \"url\": \"https:\/\/saveyourclicks.com\/assets\/logo.png\" } },\n  \"datePublished\": \"2026-06-01\",\n  \"dateModified\": \"2026-06-01\",\n  \"articleSection\": \"Build Diaries\",\n  \"keywords\": \"react three fiber, 3d website, scrollytelling, seo friendly 3d website, prerendering, dynamic rendering, crawlable javascript, core web vitals, code splitting, build diary\"\n}\n<\/script>\n<script type=\"application\/ld+json\">\n{\n  \"@context\": \"https:\/\/schema.org\",\n  \"@type\": \"FAQPage\",\n  \"mainEntity\": [\n    { \"@type\":\"Question\",\"name\":\"Why React Three Fiber instead of a single HTML file with Three.js?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"A single-file Three.js scene is fine for a particle backdrop, but the moment you want the page's real content \u2014 cards, headings, links \u2014 to be part of the 3D motion, hand-managing the DOM and the render loop in one file gets unmanageable. React Three Fiber (R3F) gives you a real component tree, a proper build, and clean separation between the WebGL scene and the DOM overlay. That structure is what let the cards become part of the animation instead of static boxes behind it.\"}},\n    { \"@type\":\"Question\",\"name\":\"Does a 3D WebGL homepage hurt SEO?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"It doesn't have to, but keeping content as 'real DOM' isn't enough on its own. With client-side React the DOM is built by JavaScript, so the raw HTML a crawler first receives is an empty div#root. When this build became the live homepage, the fix was a build-time prerender: headless Chrome renders the page once and the full HTML \u2014 headline, sections, all 45 links \u2014 gets baked into index.html. Crawlers and the first paint get the real content immediately while the browser hydrates the 3D on top. Live, the homepage went from 967 empty bytes to 16KB of crawlable HTML with robots index,follow.\"}},\n    { \"@type\":\"Question\",\"name\":\"Why native browser scroll instead of drei's ScrollControls?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"drei's ScrollControls builds its own custom scroll container. In this build it didn't mount cleanly, so the content disappeared and the page stopped scrolling entirely. Switching to native browser scroll with a fixed canvas behind it fixed both problems at once \u2014 scroll always works, content lives in the real DOM, and it dropped the bundle from 622 modules to 57. A library's scroll hijack is rarely worth giving up native scroll.\"}},\n    { \"@type\":\"Question\",\"name\":\"What's the performance cost of a React Three Fiber homepage?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"The R3F + Three + postprocessing bundle landed around 1MB raw \/ 283KB gzip \u2014 heavy to load up front. Cheap wins kept the runtime smooth: skip the per-frame blur and glow work for off-screen cards, cap devicePixelRatio at 1.5, drop particle count from 7,200 to 5,000, and run Bloom on desktop only. At cutover we code-split the 3D: Three.js and the R3F layer became async chunks loaded only when the lazy backdrop mounts, so first-load JS dropped from 283KB to 49KB gzip \u2014 content paints first, the WebGL scene streams in after.\"}},\n    { \"@type\":\"Question\",\"name\":\"How do you prerender a React Three Fiber site without breaking the 3D?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"A small post-build script boots the built site in headless Chrome, waits for the content to mount, and writes the rendered HTML back into index.html. Two gotchas: headless Chrome usually has no GPU, so the Canvas throws and \u2014 if it shares a React tree with your content \u2014 takes the whole page down, so wrap it in an error boundary that lets the backdrop drop out while the content still renders. And strip any runtime-injected third-party tags (like the GTM script injected on load) from the snapshot so they don't double-fire on the client. The canvas never needs to be in the HTML \u2014 only the content does.\"}},\n    { \"@type\":\"Question\",\"name\":\"Can I reuse this scroll-driven 3D design on another site?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"Yes. The whole thing is one reusable pattern: a fixed WebGL canvas, native scroll, sticky sections that 'assemble' their cards as you scroll, and a per-frame DOM motion engine that flies each card in from depth along a curved arc. The end of this build produced a short English prompt describing exactly that, so the same scrollytelling homepage can be cloned onto another site in one shot.\"}}\n  ]\n}\n<\/script>\n\n<article class=\"bd-article\">\n\n  <!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 HERO \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n  <header class=\"bd-hero\">\n    <div class=\"bd-container\">\n      <p class=\"bd-eyebrow\"><span class=\"bd-dot\" aria-hidden=\"true\"><\/span>Build Diary \u00b7 05 \u00b7 <time datetime=\"2026-06-01\">June 1, 2026<\/time><\/p>\n      <p class=\"bd-hero-title\">Turning a flat homepage into a <span class=\"bd-grad\">scroll-driven 3D world<\/span> \u2014 every minute timestamped.<\/p>\n      <p class=\"bd-lead\">Six hours. Three full rewrites. Two bugs and two breakthroughs. The exact build log of rebuilding the saveyourclicks homepage as an immersive React Three Fiber experience with Claude \u2014 particles that morph on scroll, cards that fly in from deep space, and the one idea that finally made it feel alive. <strong>Then \u2014 the same day, right after publishing this \u2014 the cutover the diary kept warning about:<\/strong> turning a client-rendered WebGL page into one that&#8217;s fully crawlable, indexable, and 83% lighter on first load.<\/p>\n\n      <dl class=\"bd-honesty\" aria-label=\"Build summary\">\n        <div><dt>Build time<\/dt><dd><span class=\"bd-num\">6h 20m<\/span><\/dd><\/div>\n        <div><dt>Bugs fixed<\/dt><dd><span class=\"bd-num\">2<\/span><\/dd><\/div>\n        <div><dt>Breakthroughs<\/dt><dd><span class=\"bd-num\">2<\/span><\/dd><\/div>\n        <div><dt>Dead ends<\/dt><dd><span class=\"bd-num\">2<\/span><\/dd><\/div>\n      <\/dl>\n    <\/div>\n  <\/header>\n\n  <!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 TIMELINE \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n  <main class=\"bd-timeline\">\n    <div class=\"bd-container\">\n\n      <ol class=\"bd-rail\" aria-label=\"Build timeline\">\n\n        <!-- \u2500\u2500 Milestone: start \u2500\u2500 -->\n        <li class=\"bd-event bd-milestone\" id=\"start\">\n          <time class=\"bd-event-time\" datetime=\"PT0M\">+0:00<\/time>\n          <div class=\"bd-event-card\">\n            <p class=\"bd-event-tag\">Start<\/p>\n            <h2>The plan: make the homepage feel like the 3D sites all over Instagram.<\/h2>\n            <p>Every other reel right now is a scroll-driven 3D website built with AI. I wanted that for the saveyourclicks homepage \u2014 particles, depth, motion \u2014 but without wrecking SEO. Two ground rules from minute one: build it on a <strong>test directory<\/strong> (<code>\/home-v2\/<\/code>) with the real homepage&#8217;s content first, and keep all the important text and links \u2014 nav, footer, headings \u2014 as real DOM <em>outside<\/em> the animation so nothing that matters lives inside a canvas. If it turned out great, we&#8217;d cut over later.<\/p>\n          <\/div>\n        <\/li>\n\n        <!-- \u2500\u2500 Dead end 01 \u2500\u2500 -->\n        <li class=\"bd-event bd-deadend\" id=\"deadend-01\">\n          <time class=\"bd-event-time\" datetime=\"PT22M\">+0:22<\/time>\n          <div class=\"bd-event-card\">\n            <p class=\"bd-event-tag\"><span class=\"bd-pill bd-pill-muted\">Dead end<\/span><span class=\"bd-cost\">\u23f1 ~20 min<\/span><\/p>\n            <h2>A single-file Three.js scene with a particle hero.<\/h2>\n            <p>First swing: one self-contained HTML file, a Three.js particle field behind the hero section. It looked like motion \u2014 but scroll down and everything below was a completely ordinary page. &#8220;Too little,&#8221; was the verdict. &#8220;There&#8217;s no idea behind it.&#8221;<\/p>\n            <p class=\"bd-deadend-lesson\"><strong>What I learned:<\/strong> a particle backdrop isn&#8217;t a concept. A few dots drifting behind a static page reads as a screensaver, not an experience. The motion has to <em>mean<\/em> something across the whole scroll, or it&#8217;s just decoration.<\/p>\n          <\/div>\n        <\/li>\n\n        <!-- \u2500\u2500 Dead end 02 \u2500\u2500 -->\n        <li class=\"bd-event bd-deadend\" id=\"deadend-02\">\n          <time class=\"bd-event-time\" datetime=\"PT43M\">+0:43<\/time>\n          <div class=\"bd-event-card\">\n            <p class=\"bd-event-tag\"><span class=\"bd-pill bd-pill-muted\">Dead end<\/span><span class=\"bd-cost\">\u23f1 ~20 min<\/span><\/p>\n            <h2>Full-page morphing particles \u2014 still bolted onto a static page.<\/h2>\n            <p>Second swing, still single-file: one particle system that morphs across the whole scroll. Better, but the feedback was sharp and right \u2014 &#8220;the particles open and close, but the elements behind them just sit there.&#8221; The cards weren&#8217;t sharp, didn&#8217;t move, didn&#8217;t rotate around any axis. The background was alive; the page was dead.<\/p>\n            <figure class=\"bd-figure\">\n              <img src=\"https:\/\/saveyourclicks.com\/blog\/wp-content\/uploads\/2026\/06\/build-diary-building-3d-homepage-react-three-fiber-claude-1.png\" alt=\"Work-in-progress hero of the test homepage \u2014 the headline 'AI-Powered SEO Tools and Claude MCP Integrations' over a near-empty dark background with only a few scattered particles, the cards flat and motionless.\" width=\"1280\" height=\"640\" loading=\"lazy\" decoding=\"async\">\n              <figcaption>The single-file phase: particles drift behind the hero, but the headline and cards are flat and static. Alive in the background, dead in the foreground.<\/figcaption>\n            <\/figure>\n            <p class=\"bd-deadend-lesson\"><strong>What I learned:<\/strong> you can&#8217;t bolt motion onto static DOM cards and call it 3D. If the elements aren&#8217;t <em>part of<\/em> the animation, no amount of background particles will rescue it. That meant abandoning the hand-written single file entirely.<\/p>\n          <\/div>\n        <\/li>\n\n        <!-- \u2500\u2500 Eureka 01 \u2500\u2500 -->\n        <li class=\"bd-event bd-eureka\" id=\"eureka-01\">\n          <time class=\"bd-event-time\" datetime=\"PT54M\">+0:54<\/time>\n          <div class=\"bd-event-card\">\n            <p class=\"bd-event-tag\"><span class=\"bd-pill bd-pill-eureka\">Eureka \u2726<\/span><\/p>\n            <h2>Stop decorating the page. Make the whole page one 3D scene.<\/h2>\n            <p class=\"bd-eureka-shift\">The shift: from &#8220;a page with a particle background&#8221; to &#8220;a single scroll-scrubbed 3D scene the DOM rides on top of.&#8221;<\/p>\n            <p>We threw out the single file and rebuilt it as a real <strong>React Three Fiber<\/strong> project \u2014 Vite, a proper component tree, an actual build step. The canvas becomes fully pinned and full-screen; scroll <em>scrubs<\/em> the entire experience. The camera starts in a distant galaxy (<code>z=48<\/code>) and flies through space as you scroll, while the particle field morphs from chaos into clusters into a network. Same mechanic as those Instagram sites \u2014 but now it&#8217;s an engineered scene, not a hand-managed file.<\/p>\n          <\/div>\n        <\/li>\n\n        <!-- \u2500\u2500 Bug 01 \u2500\u2500 -->\n        <li class=\"bd-event bd-bug\" id=\"bug-01\">\n          <time class=\"bd-event-time\" datetime=\"PT59M\">+0:59<\/time>\n          <div class=\"bd-event-card\">\n            <p class=\"bd-event-tag\"><span class=\"bd-pill bd-pill-warn\">Bug 01<\/span><span class=\"bd-cost\">\u23f1 ~6 min \u00b7 \ud83e\udde0 reusable lesson<\/span><\/p>\n            <h2>The page that wouldn&#8217;t scroll \u2014 drei&#8217;s ScrollControls.<\/h2>\n            <dl class=\"bd-bug-detail\">\n              <div><dt>Symptom<\/dt><dd>The canvas and nav rendered, but the page content was gone and scrolling was completely dead. No FPS readout, no movement \u2014 a frozen screen.<\/dd><\/div>\n              <div><dt>Root cause<\/dt><dd>I&#8217;d wired scroll through drei&#8217;s <code>&lt;ScrollControls&gt;<\/code>, which builds its own custom scroll container. It didn&#8217;t mount correctly here, so there was no working scroll context \u2014 which meant neither the overlaid content nor the scroll itself worked.<\/dd><\/div>\n              <div><dt>Fix<\/dt><dd>Drop <code>&lt;ScrollControls&gt;<\/code> entirely. Go back to <strong>native browser scroll<\/strong> with the R3F canvas <code>position:fixed<\/code> behind it. Scroll is guaranteed to work, content stays in the real DOM (visible even if WebGL fails), and the bundle dropped from 622 modules to 57.<\/dd><\/div>\n            <\/dl>\n            <figure class=\"bd-figure\">\n              <img src=\"https:\/\/saveyourclicks.com\/blog\/wp-content\/uploads\/2026\/06\/build-diary-building-3d-homepage-react-three-fiber-claude-2.png\" alt=\"The test homepage frozen \u2014 only the top navigation bar is visible over an almost entirely black page, with no content and a dead scrollbar, captured at the moment drei's ScrollControls failed to mount.\" width=\"1280\" height=\"720\" loading=\"lazy\" decoding=\"async\">\n              <figcaption>&#8220;Scroll doesn&#8217;t work at all.&#8221; drei&#8217;s custom scroll container never mounted \u2014 nav present, content gone, page frozen.<\/figcaption>\n            <\/figure>\n          <\/div>\n        <\/li>\n\n        <!-- \u2500\u2500 Eureka 02 \u2500\u2500 -->\n        <li class=\"bd-event bd-eureka\" id=\"eureka-02\">\n          <time class=\"bd-event-time\" datetime=\"PT361M\">+6:01<\/time>\n          <div class=\"bd-event-card\">\n            <p class=\"bd-event-tag\"><span class=\"bd-pill bd-pill-eureka\">Eureka \u2726<\/span><\/p>\n            <h2>Cards as beads on a cosmic thread \u2014 assembled <em>by<\/em> scroll.<\/h2>\n            <p class=\"bd-eureka-shift\">The shift: cards aren&#8217;t laid out and then revealed; scroll <em>builds<\/em> them \u2014 each one a bead flying in along a cosmic thread.<\/p>\n            <p>Even with native scroll working and particles morphing, one note kept coming back: &#8220;I want the cards to <em>appear<\/em> with scroll, not already be sitting at the bottom of the page while I just scroll past them.&#8221; That reframed everything. Instead of pre-placing cards and fading them, each section became a tall <code>sticky<\/code> block that spends its scroll distance <strong>assembling<\/strong> its cards in place. A per-frame DOM motion engine flies each card in from deep space (<code>translateZ \u2212560<\/code>) along a curved arc from alternating sides, blur\u2192sharp, with a blue\/violet glow trailing it mid-flight \u2014 then settles into a subtle idle float and tilt-toward-the-cursor. Beads on a thread, strung by the scrollbar.<\/p>\n          <\/div>\n        <\/li>\n\n        <!-- \u2500\u2500 Bug 02 \u2500\u2500 -->\n        <li class=\"bd-event bd-bug\" id=\"bug-02\">\n          <time class=\"bd-event-time\" datetime=\"PT369M\">+6:09<\/time>\n          <div class=\"bd-event-card\">\n            <p class=\"bd-event-tag\"><span class=\"bd-pill bd-pill-warn\">Bug 02<\/span><span class=\"bd-cost\">\u23f1 ~3 min \u00b7 \ud83e\udde0 reusable lesson<\/span><\/p>\n            <h2>The animation that silently vanished \u2014 one missing <code>const<\/code>.<\/h2>\n            <dl class=\"bd-bug-detail\">\n              <div><dt>Symptom<\/dt><dd>After rewriting the card-entry math, all the card animation disappeared. The cards just sat there, frozen \u2014 no error dialog, no obvious crash, nothing in the design that pointed at why.<\/dd><\/div>\n              <div><dt>Root cause<\/dt><dd>While editing the entry logic by hand I accidentally dropped one line \u2014 <code>const start = el._order * STAGGER<\/code>. With <code>start<\/code> undefined, the animation loop threw a <code>ReferenceError<\/code> on <em>every frame<\/em>, which silently killed the entire <code>requestAnimationFrame<\/code> loop. One missing declaration took down all the motion.<\/dd><\/div>\n              <div><dt>Fix<\/dt><dd>Restore the line. The whole loop came back to life. <strong>Reusable lesson:<\/strong> when motion suddenly vanishes with no visible crash, suspect a JavaScript throw <em>inside<\/em> the rAF loop \u2014 a single per-frame exception takes the whole animation down quietly.<\/dd><\/div>\n            <\/dl>\n          <\/div>\n        <\/li>\n\n        <!-- \u2500\u2500 Milestone: shipped \u2500\u2500 -->\n        <li class=\"bd-event bd-milestone bd-shipped\" id=\"shipped\">\n          <time class=\"bd-event-time\" datetime=\"PT380M\">+6:20<\/time>\n          <div class=\"bd-event-card\">\n            <p class=\"bd-event-tag\">Shipped<\/p>\n            <h2>Live on the test directory \u2014 smooth, glowing, and reusable.<\/h2>\n            <p>The last hour was polish, not features. To kill the lag without touching the look: skip the blur and glow work for off-screen sections, cap <code>devicePixelRatio<\/code> at 1.5, drop particles from 7,200 to 5,000. Then the final coat of paint \u2014 cinematic <strong>Bloom<\/strong> so the particles and card halos actually glow (version-pinned <code>postprocessing<\/code> because the current release needs a newer Three.js than the scene runs, desktop-only to stay light). It went live at <code>\/home-v2\/<\/code> as a <code>noindex<\/code> test: native scroll, sticky sections that assemble cards, a dramatic arc entry from deep space, a scroll-scrubbed camera fly-through, and Bloom on top \u2014 283KB gzip. The build even handed back a short English prompt that recreates the whole pattern, so the same scrollytelling homepage can be cloned onto another site in one shot.<\/p>\n            <figure class=\"bd-figure\">\n              <img src=\"https:\/\/saveyourclicks.com\/blog\/wp-content\/uploads\/2026\/06\/build-diary-building-3d-homepage-react-three-fiber-claude-3.png\" alt=\"The finished test homepage hero \u2014 a deep starfield of blue and violet particles filling the screen with the headline and three stat counters (144 AI Tools, 5 SEO Tools, 3 Google Integrations) floating in the center.\" width=\"1280\" height=\"640\" loading=\"lazy\" decoding=\"async\">\n              <figcaption>The shipped hero: a full WebGL starfield with the camera flying through it, the stats floating in 3D, Bloom making the particles glow.<\/figcaption>\n            <\/figure>\n            <figure class=\"bd-figure\">\n              <img src=\"https:\/\/saveyourclicks.com\/blog\/wp-content\/uploads\/2026\/06\/build-diary-building-3d-homepage-react-three-fiber-claude-4.png\" alt=\"The finished tools section \u2014 Keyword Clustering, Heading Gap, Keyword Gap and other cards arranged in 3D, each surrounded by a cluster of glowing blue particles, having flown in from depth as the user scrolled.\" width=\"1280\" height=\"640\" loading=\"lazy\" decoding=\"async\">\n              <figcaption>The tools section after scroll-assembly: each card has flown in from deep space along its arc and settled beside its own particle cluster.<\/figcaption>\n            <\/figure>\n          <\/div>\n        <\/li>\n\n        <!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 PART 2 \u00b7 THE CUTOVER \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n\n        <!-- \u2500\u2500 Milestone: part 2 \u2500\u2500 -->\n        <li class=\"bd-event bd-milestone\" id=\"part-2\">\n          <time class=\"bd-event-time\" datetime=\"2026-06-01\">\u2193 same day<\/time>\n          <div class=\"bd-event-card\">\n            <p class=\"bd-event-tag\">Part 2 \u00b7 The cutover<\/p>\n            <h2>The diary said we&#8217;d need prerendering before going live. The same day, we did.<\/h2>\n            <p>Right after publishing the build log above, the verdict came in: it looked good enough to <em>be<\/em> the homepage, not just a test directory. So we cut over. But everything this diary kept flagging in passing \u2014 &#8220;R3F renders client-side,&#8221; &#8220;it would need prerender or SSR,&#8221; &#8220;noindex on purpose because it duplicates the live homepage&#8221; \u2014 suddenly stopped being a footnote and became the actual job. Here&#8217;s the hour that turned the test build into the real, indexable saveyourclicks.com.<\/p>\n          <\/div>\n        <\/li>\n\n        <!-- \u2500\u2500 Bug 03: the SEO trap \u2500\u2500 -->\n        <li class=\"bd-event bd-bug\" id=\"bug-03\">\n          <time class=\"bd-event-time\" datetime=\"PT5M\">+0:05<\/time>\n          <div class=\"bd-event-card\">\n            <p class=\"bd-event-tag\"><span class=\"bd-pill bd-pill-warn\">Bug 03<\/span><span class=\"bd-cost\">\u23f1 ~5 min \u00b7 \ud83e\udde0 reusable lesson<\/span><\/p>\n            <h2>The homepage Google would have seen: an empty <code>&lt;div&gt;<\/code>.<\/h2>\n            <dl class=\"bd-bug-detail\">\n              <div><dt>Symptom<\/dt><dd>The page <em>looked<\/em> full of content and links in the browser \u2014 but <code>curl<\/code> as Googlebot returned 967 bytes: <code>&lt;div id=\"root\"&gt;&lt;\/div&gt;<\/code> and nothing else. No headline, no nav, no links. And the test build still carried <code>&lt;meta name=\"robots\" content=\"noindex,nofollow\"&gt;<\/code>. Swapping it onto <code>\/<\/code> as-is would have told Google to <strong>deindex the homepage<\/strong>.<\/dd><\/div>\n              <div><dt>Root cause<\/dt><dd>This is the catch the whole diary danced around: keeping content as &#8220;real DOM&#8221; only helps if that DOM exists <em>in the HTML<\/em>. With client-side React, the DOM is built by JavaScript after load \u2014 so crawlers that don&#8217;t execute JS (Bing, social and AI bots) and the all-important first paint see an empty shell. &#8220;Crawlable in principle&#8221; isn&#8217;t crawlable.<\/dd><\/div>\n              <div><dt>Fix<\/dt><dd>Stop shipping an empty shell. Render the real DOM <em>at build time<\/em> and ship that \u2014 see the next step.<\/dd><\/div>\n            <\/dl>\n          <\/div>\n        <\/li>\n\n        <!-- \u2500\u2500 Eureka 03: prerender \u2500\u2500 -->\n        <li class=\"bd-event bd-eureka\" id=\"eureka-03\">\n          <time class=\"bd-event-time\" datetime=\"PT15M\">+0:15<\/time>\n          <div class=\"bd-event-card\">\n            <p class=\"bd-event-tag\"><span class=\"bd-pill bd-pill-eureka\">Eureka \u2726<\/span><\/p>\n            <h2>Prerender at build time \u2014 ship the rendered DOM, hydrate the 3D after.<\/h2>\n            <p class=\"bd-eureka-shift\">The shift: the canvas doesn&#8217;t need to be in the HTML \u2014 the <em>content<\/em> does. So render once, headless, and bake the result into <code>index.html<\/code>.<\/p>\n            <p>We added a tiny post-build step: after <code>vite build<\/code>, a script boots the site in <strong>headless Chrome<\/strong> (puppeteer), waits for React to mount the real content, then writes <code>document.documentElement.outerHTML<\/code> back into <code>dist\/index.html<\/code>. No SSR rewrite, no framework migration \u2014 one <code>prerender.mjs<\/code>. The shipped HTML now contains the full headline, every section, and all 45 links on the <em>first byte<\/em>; the browser still boots React over it and the 3D comes alive exactly as before. Same approach Google calls &#8220;dynamic rendering,&#8221; except everyone \u2014 bots and humans \u2014 gets the identical pre-rendered HTML, so there&#8217;s nothing cloaked.<\/p>\n          <\/div>\n        <\/li>\n\n        <!-- \u2500\u2500 Bug 04: WebGL in headless \u2500\u2500 -->\n        <li class=\"bd-event bd-bug\" id=\"bug-04\">\n          <time class=\"bd-event-time\" datetime=\"PT25M\">+0:25<\/time>\n          <div class=\"bd-event-card\">\n            <p class=\"bd-event-tag\"><span class=\"bd-pill bd-pill-warn\">Bug 04<\/span><span class=\"bd-cost\">\u23f1 ~10 min \u00b7 \ud83e\udde0 reusable lesson<\/span><\/p>\n            <h2>Headless Chrome has no GPU \u2014 and the dead canvas took the whole page down.<\/h2>\n            <dl class=\"bd-bug-detail\">\n              <div><dt>Symptom<\/dt><dd>First prerender run captured\u2026 nothing. <code>#root<\/code> was empty in the snapshot, the content selector timed out. The console was full of <code>THREE.WebGLRenderer: A WebGL context could not be created<\/code>.<\/dd><\/div>\n              <div><dt>Root cause<\/dt><dd>Headless Chrome couldn&#8217;t create a WebGL context, so <code>&lt;Canvas&gt;<\/code> threw \u2014 and because the canvas and the content were siblings in one React tree, that throw unmounted the <em>entire<\/em> tree. The decorative backdrop was taking the real content down with it. (Worth noting: that meant any real visitor without WebGL would have seen a blank page too.)<\/dd><\/div>\n              <div><dt>Fix<\/dt><dd>Wrap the canvas in a one-line <strong>error boundary<\/strong> \u2014 if WebGL fails, the backdrop quietly drops out and the content renders untouched. That unblocked the prerender <em>and<\/em> turned a latent blank-screen bug for low-end devices into graceful degradation. <strong>Reusable lesson:<\/strong> never let a decorative WebGL layer share a crash with your content \u2014 isolate it behind a boundary.<\/dd><\/div>\n            <\/dl>\n          <\/div>\n        <\/li>\n\n        <!-- \u2500\u2500 Eureka 04: keep the head, split the bundle \u2500\u2500 -->\n        <li class=\"bd-event bd-eureka\" id=\"eureka-04\">\n          <time class=\"bd-event-time\" datetime=\"PT40M\">+0:40<\/time>\n          <div class=\"bd-event-card\">\n            <p class=\"bd-event-tag\"><span class=\"bd-pill bd-pill-eureka\">Eureka \u2726<\/span><\/p>\n            <h2>Keep the real SEO head, then split the bundle: 283KB \u2192 49KB on first load.<\/h2>\n            <p class=\"bd-eureka-shift\">The shift: the new design is only the <code>&lt;body&gt;<\/code>. The homepage&#8217;s <em>head<\/em> \u2014 its years of SEO \u2014 has to come along untouched.<\/p>\n            <p>A na\u00efve file swap would have nuked the old static homepage&#8217;s entire <code>&lt;head&gt;<\/code>. So we ported it wholesale into the build template: real <code>&lt;title&gt;<\/code>, <code>robots: index,follow<\/code>, self-referencing canonical, Open Graph, Twitter cards, <code>hreflang<\/code> for en\/fa\/ar, the WebSite JSON-LD, and the GTM container \u2014 with the prerender stripping the runtime-injected GTM tag so analytics doesn&#8217;t double-fire. Then the performance note this diary promised: the 1MB \/ 283KB-gzip bundle got <strong>code-split<\/strong> \u2014 Three.js and the R3F layer became async chunks loaded only when the lazy backdrop mounts. First-load JS dropped from <strong>283KB to 49KB gzip (\u221283%)<\/strong>; the content paints, the 3D arrives after.<\/p>\n          <\/div>\n        <\/li>\n\n        <!-- \u2500\u2500 Milestone: shipped live \u2500\u2500 -->\n        <li class=\"bd-event bd-milestone bd-shipped\" id=\"shipped-live\">\n          <time class=\"bd-event-time\" datetime=\"PT55M\">+0:55<\/time>\n          <div class=\"bd-event-card\">\n            <p class=\"bd-event-tag\">Shipped \u2014 for real<\/p>\n            <h2>Live at the real homepage \u2014 same design, now crawlable <em>and<\/em> indexable.<\/h2>\n            <p>The 3D scroll experience is now <code>saveyourclicks.com\/<\/code> itself, not a test directory. As Googlebot, the homepage went from <strong>967 empty bytes to 16KB with 45 links and 17 headings<\/strong> in the raw HTML, <code>robots: index,follow<\/code>, full Open Graph and JSON-LD intact \u2014 and a 49KB first-load instead of 283KB. The old static homepage is kept as a one-command rollback, and the sitemap&#8217;s <code>lastmod<\/code> was bumped so Google re-crawls. The diary&#8217;s opening rule \u2014 &#8220;without wrecking SEO&#8221; \u2014 held all the way to production: every word and link a search engine or AI crawler needs is in the first response, and the galaxy still flies by on scroll.<\/p>\n          <\/div>\n        <\/li>\n\n      <\/ol>\n    <\/div>\n  <\/main>\n\n  <!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 TIME LEDGER \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n  <section class=\"bd-ledger-section\" id=\"ledger\">\n    <div class=\"bd-container\">\n      <header class=\"bd-section-header\">\n        <p class=\"bd-section-num\">The honest accounting<\/p>\n        <h2>Where the 6 hours <em>actually<\/em> went.<\/h2>\n        <p class=\"bd-section-lead\">The bugs were quick. The real cost was iterating the <em>feel<\/em> of the motion \u2014 and I was building blind, so every change meant rebuild, deploy, and wait for a human to tell me if it landed.<\/p>\n      <\/header>\n\n      <dl class=\"bd-ledger\" aria-label=\"Time spent by phase\">\n        <div class=\"bd-ledger-row\" style=\"--w:8%\">\n          <dt>Setup<\/dt>\n          <dd><span class=\"bd-ledger-bar bd-ledger-setup\"><\/span><span class=\"bd-ledger-num\">~30m<\/span><\/dd>\n        <\/div>\n        <div class=\"bd-ledger-row\" style=\"--w:11%\">\n          <dt>Debug bugs<\/dt>\n          <dd><span class=\"bd-ledger-bar bd-ledger-bug\"><\/span><span class=\"bd-ledger-num\">~40m<\/span><\/dd>\n        <\/div>\n        <div class=\"bd-ledger-row\" style=\"--w:18%\">\n          <dt>Dead ends<\/dt>\n          <dd><span class=\"bd-ledger-bar bd-ledger-deadend\"><\/span><span class=\"bd-ledger-num\">~68m<\/span><\/dd>\n        <\/div>\n        <div class=\"bd-ledger-row\" style=\"--w:8%\">\n          <dt>Eureka moments<\/dt>\n          <dd><span class=\"bd-ledger-bar bd-ledger-eureka\"><\/span><span class=\"bd-ledger-num\">~30m (worth it)<\/span><\/dd>\n        <\/div>\n        <div class=\"bd-ledger-row\" style=\"--w:55%\">\n          <dt>Polish &amp; ship<\/dt>\n          <dd><span class=\"bd-ledger-bar bd-ledger-polish\"><\/span><span class=\"bd-ledger-num\">~210m<\/span><\/dd>\n        <\/div>\n      <\/dl>\n      <p class=\"bd-ledger-takeaway\"><strong>Takeaway:<\/strong> more than half the time went into iterating the motion feel through blind rebuild-deploy-feedback loops; jumping straight to &#8220;full R3F + native scroll + a per-frame DOM motion engine&#8221; instead of two single-file detours would have shortcut the whole first half.<\/p>\n    <\/div>\n  <\/section>\n\n  <!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 FAQ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n  <section class=\"bd-faq-section\" id=\"faq\">\n    <div class=\"bd-container\">\n      <header class=\"bd-section-header\">\n        <p class=\"bd-section-num\">Questions you might have<\/p>\n        <h2>The five <em>real<\/em> questions about this build.<\/h2>\n      <\/header>\n\n      <div class=\"bd-faq\">\n        <details>\n          <summary>Why React Three Fiber instead of a single HTML file with Three.js?<\/summary>\n          <p>A single-file Three.js scene is fine for a particle backdrop, but the moment you want the page&#8217;s real content \u2014 cards, headings, links \u2014 to be part of the 3D motion, hand-managing the DOM and the render loop in one file gets unmanageable. <strong>React Three Fiber<\/strong> gives you a real component tree, a proper build, and clean separation between the WebGL scene and the DOM overlay. That structure is what let the cards become part of the animation instead of static boxes behind it.<\/p>\n        <\/details>\n        <details>\n          <summary>Does a 3D WebGL homepage hurt SEO?<\/summary>\n          <p>It doesn&#8217;t have to \u2014 but keeping content as &#8220;real DOM&#8221; isn&#8217;t enough on its own. With client-side React, that DOM is built by JavaScript, so the raw HTML a crawler first receives is an empty <code>&lt;div id=\"root\"&gt;<\/code>. When this build became the live homepage, the fix was a build-time <strong>prerender<\/strong>: headless Chrome renders the page once and the full HTML \u2014 headline, sections, all 45 links \u2014 gets baked into <code>index.html<\/code>. Crawlers and the first paint get the real content immediately; the browser still hydrates the 3D on top. Live, the homepage went from 967 empty bytes to 16KB of crawlable HTML with <code>robots: index,follow<\/code>.<\/p>\n        <\/details>\n        <details>\n          <summary>Why native browser scroll instead of drei&#8217;s ScrollControls?<\/summary>\n          <p>drei&#8217;s <code>ScrollControls<\/code> builds its own custom scroll container. In this build it didn&#8217;t mount cleanly, so the content disappeared and the page stopped scrolling entirely. Switching to native browser scroll with a fixed canvas behind it fixed both problems at once \u2014 scroll always works, content lives in the real DOM, and it dropped the bundle from 622 modules to 57. A library&#8217;s scroll hijack is rarely worth giving up native scroll.<\/p>\n        <\/details>\n        <details>\n          <summary>What&#8217;s the performance cost of a React Three Fiber homepage?<\/summary>\n          <p>The R3F + Three + postprocessing bundle landed around 1MB raw \/ 283KB gzip \u2014 heavy to load up front. Cheap wins kept the runtime smooth: skip the per-frame blur and glow work for off-screen cards, cap <code>devicePixelRatio<\/code> at 1.5, drop particle count from 7,200 to 5,000, and run Bloom on desktop only. Then at cutover we <strong>code-split<\/strong> the 3D: Three.js and the R3F layer became async chunks loaded only when the lazy backdrop mounts, so first-load JS dropped from 283KB to 49KB gzip \u2014 the content paints first and the WebGL scene streams in after.<\/p>\n        <\/details>\n        <details>\n          <summary>How do you prerender a React Three Fiber site without breaking the 3D?<\/summary>\n          <p>A small post-build script (<code>prerender.mjs<\/code>) boots the built site in headless Chrome, waits for the content to mount, and writes the rendered HTML back into <code>index.html<\/code>. Two gotchas: headless Chrome usually has no GPU, so <code>&lt;Canvas&gt;<\/code> throws and \u2014 if it shares a React tree with your content \u2014 takes the whole page down; wrap it in an <strong>error boundary<\/strong> so the backdrop drops out and the content still renders. And strip any runtime-injected third-party tags (like the GTM script the page injects on load) from the snapshot so they don&#8217;t double-fire on the client. The canvas itself never needs to be in the HTML \u2014 only the content does.<\/p>\n        <\/details>\n        <details>\n          <summary>Can I reuse this scroll-driven 3D design on another site?<\/summary>\n          <p>Yes. The whole thing is one reusable pattern: a fixed WebGL canvas, native scroll, sticky sections that &#8220;assemble&#8221; their cards as you scroll, and a per-frame DOM motion engine that flies each card in from depth along a curved arc. The end of this build produced a short English prompt describing exactly that, so the same scrollytelling homepage can be cloned onto another site in one shot.<\/p>\n        <\/details>\n      <\/div>\n    <\/div>\n  <\/section>\n\n  <!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 FOOTER \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n  <footer class=\"bd-footer\">\n    <div class=\"bd-container\">\n      <p>Build Diaries \u00b7 A series on shipping AI-built work to production \u2014 every minute timestamped, every dead end shown, every breakthrough named.<\/p>\n      <p class=\"bd-footer-meta\"><time datetime=\"2026-06-01\">June 1, 2026<\/time> \u00b7 by <a href=\"https:\/\/saveyourclicks.com\/\" rel=\"author\">Yusof Ansari<\/a><\/p>\n    <\/div>\n  <\/footer>\n\n<\/article>\n\n<script src=\"\/blog\/wp-content\/uploads\/build-diaries\/building-3d-homepage-react-three-fiber-claude.js?v=33ca7bfc\" defer><\/script>\n\n\n","protected":false},"excerpt":{"rendered":"<p>A timestamped build log: rebuilding a homepage as a scroll-driven 3D website with React Three Fiber and Claude \u2014 3 rewrites, 2 bugs, and the scrollytelling idea that clicked.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"categories":[103],"tags":[],"class_list":["post-460","post","type-post","status-publish","format-standard","hentry","category-build-diaries"],"lang":"en","translations":{"en":460},"pll_sync_post":[],"_links":{"self":[{"href":"https:\/\/saveyourclicks.com\/blog\/wp-json\/wp\/v2\/posts\/460","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/saveyourclicks.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/saveyourclicks.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/saveyourclicks.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/saveyourclicks.com\/blog\/wp-json\/wp\/v2\/comments?post=460"}],"version-history":[{"count":1,"href":"https:\/\/saveyourclicks.com\/blog\/wp-json\/wp\/v2\/posts\/460\/revisions"}],"predecessor-version":[{"id":461,"href":"https:\/\/saveyourclicks.com\/blog\/wp-json\/wp\/v2\/posts\/460\/revisions\/461"}],"wp:attachment":[{"href":"https:\/\/saveyourclicks.com\/blog\/wp-json\/wp\/v2\/media?parent=460"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/saveyourclicks.com\/blog\/wp-json\/wp\/v2\/categories?post=460"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/saveyourclicks.com\/blog\/wp-json\/wp\/v2\/tags?post=460"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}