Skip to content

    This note is about the site you are reading. athenadatalabs.com is a Vite + React single-page app: fast to build and pleasant to work in, and invisible to search by default. Here is the playbook that fixed it, and what it looked like when we ran it on ourselves.

    The problem: one page pretending to be many

    A React single-page app ships one HTML file. Every route, from your product pages to your pricing, shares the same title, description, and social preview baked into index.html. To a search engine choosing which page to rank for a query, your site looks like a single generic page. Deep links cannot rank because, as far as crawlers can tell, they do not exist.

    The good news: Googlebot executes JavaScript. You don't need to rebuild on Next.js to rank. You need to make sure that, once your JS runs, each route reports its own identity.

    Why not just move to SSR

    The obvious fix, rewriting to a server-rendered framework, is usually out of proportion to the problem. It means a full migration, new hosting requirements, and ongoing complexity, for a marketing site whose pages are mostly static prose.

    The constraint worth setting instead: make every route self-describing inside the stack you already have, with zero new runtime dependencies, in a way that cannot silently drift as the site grows to dozens of pages.

    Per-route metadata is the 80/20

    Every route needs its own title, meta description, and canonical URL, updated when the route mounts. A small component that upserts head tags via useEffect covers this without dependencies: find-or-create each meta tag, set its content, and manage a single JSON-LD script element replaced on navigation.

    Titles should lead with the page's subject, not the brand: 'Products: Aegis BI, MyBudgetNerd' beats 'Athena Data Labs | Page'. Descriptions are your ad copy in the search results, so write them for clicks, not for keyword stuffing.

    Structured data varies by page type: service pages emit schema.org Service, product pages SoftwareApplication, and articles like this one emit Article.

    Clean URLs, real routes

    Hash routing (/#/products) hides everything after the # from crawlers. Use the History API (BrowserRouter) with a catch-all rewrite to index.html on your host. On AWS Amplify that's a single 404-to-200 rewrite rule.

    Then give crawlers a map: a sitemap.xml listing every route, and a robots.txt pointing to it. Generate the sitemap at build time from the same data files that drive the routes. Then a new case study or product page is added automatically, with no separate list to forget. The class of bug where a page exists but the sitemap does not know about it simply cannot occur.

    Know what client-side rendering can't do

    Nothing but Google runs your JavaScript. Google renders the page and reads what the app sets on mount, which is why the metadata work above pays off in search. It is also the exception, not the rule. Every other client reads the raw HTML once and takes what is in it: Bing and DuckDuckGo, the social scrapers behind a shared link (Facebook, LinkedIn, Slack, iMessage), and the retrieval crawlers behind AI answers. For a client-rendered app all of them get your title and an empty div.

    We originally wrote this section as a note about social previews, and judged the fix by whether per-route Open Graph cards mattered to the funnel. That was the wrong frame, and the correction is at the end of this note.

    Ranking-wise, performance is also a signal: code-split your routes, compress images to WebP, and keep the main bundle lean. The fastest SEO win is often deleting dead kilobytes.

    The checklist

    The order we apply this in practice:

    • BrowserRouter + host rewrite rule (no hash URLs)
    • Per-route title, description, canonical, robots, and OG/Twitter tags
    • JSON-LD structured data per page type (Service, SoftwareApplication, Article)
    • sitemap.xml generated from route data + robots.txt pointing at it
    • Route-level code splitting and image compression
    • Search Console: verify, submit the sitemap, watch coverage

    What it looked like on our own site

    Every route now reports unique, accurate metadata and structured data, with a self-maintaining sitemap covering the full information architecture. It is verifiable by opening any page's head in DevTools rather than taken on faith, and indexing is tracked in Search Console rather than guessed at.

    The playbook also proved transferable: we shared this guidance with independent developers facing the same invisible-SPA problem, and several implemented it and confirmed their pages were being indexed. That is the test of a pattern: it works when someone else runs it.

    One caveat worth carrying: doing all of this correctly still did not get the site indexed. The build and the operations are two separate jobs. What was actually blocking us is the subject of the next note.

    Correction: what this playbook missed

    Two months after publishing this, we fetched our own homepage the way a crawler does, with no JavaScript, and got the title and nothing else. Not the headline, not a paragraph, not a link. Twenty-one content pages and about two hundred thousand characters of writing, and none of it was in the HTML we were serving.

    Everything above is still true and still worth doing. What it got wrong is the scope of the exception. We wrote "Googlebot executes JavaScript, so you don't need SSR" as though Google were the audience and everyone else were an edge case about link previews. In 2024 that was defensible. It is not now: Bing and DuckDuckGo never ran your JavaScript, and a meaningful share of "what software does X" now starts in an assistant, whose retrieval crawlers read raw HTML exactly once. To every one of them, a client-rendered site is a title.

    It is important that this was not a bug. Nothing crashed, nothing logged, nothing in the build had any way to report it. Metadata-only prerendering makes each route describable, which is what the checklist above measures and passes. It does not make the page readable. We had been grading ourselves on the half of the problem our tooling could see, and the half it could not see was the content.

    The fix turned out to be much smaller than the SSR migration this note talks you out of. The same post-build script that was already writing per-route metadata now also renders the React tree in Node and writes the result into the body of each file. About a hundred lines, no framework change, no new hosting requirements, and the app still boots exactly as it did. The advice we would give now: prerender the body from the start. If you already have a build step that knows your routes, you are most of the way there, and "Google can see it" is not the same as "it can be found".

    Related ServicesDashboards

    Before You Go

    Get the next field note

    One email when we publish, covering what we built, what it cost, and what went wrong.

    We use your address for this list and nothing else. Tell us to stop and we will.

    Next Step

    Ready to Deliver Your Next Data Product?

    Share your goals by email and we will respond with a practical next-step plan for scope, timeline, and delivery.

    Decision Intelligence Systems

    Athena Data Labs

    Designed, built, shipped, and run by us.

    © 2026 Athena Data Labs
    A division of Athena Analytics L.L.C