Skip to content
AEO Canon · the reference for answer-engine optimization

How to Make a Single-Page App Citable by AI

A client-rendered SPA ships an empty shell that AI crawlers can't read. To make it citable, get your content into the initial HTML — by migrating to an SSR/SSG framework, adding server rendering to your existing app, or prerendering routes to static HTML. Here are the patterns, with code.

BBurke Atkerson4 min read

A client-rendered single-page app ships an empty shell that AI crawlers can't read, so to make it citable you must get your content into the initial HTML. Three patterns do that — migrate to an SSR/SSG framework, add server rendering to your existing app, or prerender routes to static HTML — and all of them pass the same test: a curl fetch already contains your content.

The short answer

Put the content in the initial HTML. Best long-term: migrate to a meta-framework with SSR/SSG (Next.js, Nuxt, SvelteKit, Remix). Lighter retrofit: prerender your routes to static HTML and serve that. Success = curl returns your answer text.

Why can't AI cite a single-page app?

AI can't cite a typical SPA because it renders content client-side: the server sends a near-empty <div id="root"> and JavaScript fills it in the browser. A crawler that doesn't run JavaScript — which is most of them — sees the empty shell and concludes there's nothing to cite (Google's own JavaScript SEO basics cover the same rendering pitfalls). This is the access pillar failing at the rendering layer. The fix isn't to drop your framework; it's to render the citable content before it reaches the crawler.

What are the three paths to a citable SPA?

The three paths are migrating to a server-rendering framework, adding server rendering to your existing app, or prerendering routes to static HTML — in rough order of effort and durability.

Three ways to make a SPA citable
ApproachEffortBest when
Migrate to a meta-frameworkHighYou can invest in a durable, long-term fix
Add SSR to your appMediumYou want to keep your stack but render on the server
Prerender routes to static HTMLLow–mediumContent is mostly static and a rewrite isn't feasible
Dynamic rendering (serve snapshots to bots)LowA quick retrofit for an existing SPA

Path 1: migrate to an SSR/SSG framework

The most durable fix is to move to a meta-framework that renders on the server by default. In Next.js, Nuxt, SvelteKit, or Remix, page content is server-rendered (or statically generated) so it's in the HTML the crawler receives. A Next.js Server Component renders fully before it ships:

// app/products/[id]/page.tsx — server-rendered; HTML ships with content
export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const product = await getProduct(id); // runs on the server
 
  return (
    <main>
      <h1>{product.name}</h1>
      {/* In the initial HTML — crawlers and users both get it */}
      <p>{product.description}</p>
    </main>
  );
}

Path 2: add server rendering to your existing app

If you keep your current stack, render your app to an HTML string on the server and send that in the response, then hydrate in the browser. A minimal Express + React example:

// server.js — render the app to HTML on each request, then hydrate client-side
import express from "express";
import { renderToString } from "react-dom/server";
import App from "./src/App.js";
 
const app = express();
 
app.get("*", (req, res) => {
  const appHtml = renderToString(<App url={req.url} />);
  res.send(`<!doctype html>
<html>
  <head><title>My App</title></head>
  <body>
    <div id="root">${appHtml}</div>
    <script src="/static/bundle.js"></script>
  </body>
</html>`);
});
 
app.listen(3000);

Now the crawler receives real content inside #root instead of an empty div, and the browser hydrates bundle.js for interactivity. Vite, for example, provides an SSR build mode to produce the server and client bundles for this pattern.

Path 3: prerender routes to static HTML

If your content is mostly static and a rewrite isn't feasible, prerender each route to an HTML file at build time and serve those files. Tools like prerendering plugins crawl your running app and write out static HTML per route:

# Example: build, then prerender routes to static HTML (tool-specific)
npm run build
npx prerender-spa-plugin   # or react-snap, vite-plugin-ssg, etc.

Alternatively, dynamic rendering serves crawlers a cached, fully-rendered snapshot while users keep the live app — via a prerender middleware or a service. This is an accepted retrofit, not cloaking, as long as the snapshot matches what users see — though Google now recommends SSR, static rendering, or hydration instead.

Keep bot and user content identical

Serving crawlers a prerendered snapshot is fine; serving them different content to manipulate visibility is cloaking, which engines penalize. The prerendered HTML must contain the same text and answers a human gets. When in doubt, diff the bot response against the rendered page.

How do you confirm it worked?

Confirm the fix the same way a crawler would — by reading the raw HTML.

  1. 1

    Fetch as a crawler

    Run: curl -s -A "GPTBot" https://yourdomain.com/your-route | grep -i "your answer phrase"

  2. 2

    Expect a match

    If grep returns your content, it's in the initial HTML and crawlers can read it.

  3. 3

    Run the JS-disabled test

    Disable JavaScript in the browser and reload — your content should still render.

  4. 4

    Check key routes

    Test every route you want cited, not just the homepage — SPAs often render some routes and not others.

SPA citability

0 / 5

Each unchecked box is a place a competitor can beat you to the AI answer.

Where this fits in the Canon

Making a SPA citable is the rendering fix for the access pillar — turning an empty shell into HTML a crawler can read. Once the content is in the HTML, extractability governs how liftable each passage is.

Related: why JavaScript breaks AI citation eligibility, server-side vs client-side rendering, and how to check if AI crawlers can read your site.

Frequently asked questions

How do I make a single-page app citable by AI?
Get your content into the initial HTML response, because AI crawlers read raw HTML and don't run JavaScript. Three paths do this — migrate to a meta-framework with server-side rendering or static generation (Next.js, Nuxt, SvelteKit, Remix), add server rendering to your existing app, or prerender your routes to static HTML. The test of success is that a curl fetch already contains your content.
Is prerendering the same as server-side rendering?
Closely related but not identical. Server-side rendering builds the HTML fresh on each request; prerendering generates the HTML ahead of time (at build) or caches a rendered snapshot and serves that to crawlers. Both put content in the initial HTML. Prerendering is often the lighter retrofit for an existing SPA you can't fully rewrite.
Is serving prerendered HTML to bots considered cloaking?
Not when the prerendered content matches what users see. Cloaking means showing crawlers materially different content to manipulate ranking. Serving a bot a prerendered snapshot of the same page a user gets — same text, same answer — is an accepted way to make a SPA crawlable, sometimes called dynamic rendering. Keep the content identical and you're fine.
Do I have to abandon React or Vue to be citable?
No. React, Vue, and Svelte all support server-side rendering and static generation through their meta-frameworks, and standalone SSR is possible too. You keep your components and interactivity; you just render the citable content on the server and hydrate in the browser. The framework isn't the problem — shipping an empty shell is.

Last updated .

Related reading

Auto detailers should use AutomotiveBusiness (a LocalBusiness subtype) schema with accurate name, address, phone, service area, hours, and services, plus FAQ schema on answer pages — it helps engines parse who you are. Schema clarifies content for AI; it never rescues a thin site or a buried answer.

2 min read

A detailing business needs a website rebuild for AEO when it lives on social media with no real site, is slow, or lacks per-package answer-first pages and schema — because the engine can only recommend what it can read. The rebuild is the access layer everything else depends on.

2 min read

Auto repair shops should use the AutoRepair (a LocalBusiness subtype) schema with accurate name, address, phone, service area, hours, and services, plus FAQ schema on answer pages — it helps engines parse and confirm who you are. Schema clarifies content for AI; it never rescues a slow site or a buried answer.

2 min read