Skip to main content

React SPA Preset

Preset file: presets/react-spa.toml

Works with Create React App, Vite + React, and custom React apps using react-router.

Overview

React SPAs render content entirely in the browser via useEffect and fetch calls. The initial HTML is an empty <div id="root"></div> shell. PRISM waits for all network activity to settle, then extracts the fully rendered DOM.

Key Configuration

[render]
wait_for = "networkidle"
timeout_secs = 15

[render.postprocess]
enabled = true
strip_scripts = true
strip_noscript = true
strip_comments = true
strip_event_handlers = true
strip_hydration_attrs = true # Removes data-reactroot, data-reactid
resolve_lazy_images = true

[render.content_validation]
enabled = true
min_text_length = 100
require_title = true
min_html_bytes = 1024

Wait Strategy

networkidle is the recommended wait strategy for React SPAs. It waits until no network requests are in-flight for 500ms, which catches data fetching in useEffect hooks and lazy-loaded components.

If your app has long-polling or WebSocket connections that prevent networkidle from resolving, use a CSS selector instead:

[render]
wait_for = "[data-page-ready]"

Then add the attribute in your app after data loads:

useEffect(() => {
document.body.setAttribute('data-page-ready', 'true');
}, []);

Meta Tags with React Helmet

For proper SEO, use react-helmet or react-helmet-async to manage <title>, <meta>, and structured data:

import { Helmet } from 'react-helmet';

function ProductPage({ product }) {
return (
<>
<Helmet>
<title>{product.name} - MyStore</title>
<meta name="description" content={product.description} />
<script type="application/ld+json">
{JSON.stringify({
"@context": "https://schema.org",
"@type": "Product",
"name": product.name,
})}
</script>
</Helmet>
<h1>{product.name}</h1>
</>
);
}

PRISM preserves <script type="application/ld+json"> during postprocessing.

Hydration Attribute Stripping

With strip_hydration_attrs = true, PRISM removes these React-specific attributes:

AttributePurpose in ReactWhy It's Stripped
data-reactrootMarks the root elementOnly needed for client hydration
data-reactidLegacy React 15 element IDsOnly needed for client reconciliation
data-react-checksumLegacy SSR checksumOnly needed for hydration validation
data-react-helmetMarks Helmet-managed elementsOnly needed for Helmet's client-side tracking

SPA Status Codes

For soft 404 pages, signal the correct status to PRISM:

function NotFound() {
return (
<>
<Helmet>
<title>Page Not Found</title>
<meta name="render:status_code" content="404" />
</Helmet>
<h1>404 - Not Found</h1>
</>
);
}

Enable status detection in your PRISM config:

[render]
status_from_meta = true

Route Exclusions

The preset excludes static assets, API endpoints, and build artifacts:

[routes]
include = ["/**"]
exclude = [
"/api/**", "/graphql",
"**/*.js", "**/*.css", "**/*.json", "**/*.xml",
"**/*.png", "**/*.jpg", "**/*.gif", "**/*.svg", "**/*.ico",
"**/*.woff", "**/*.woff2", "**/*.ttf", "**/*.wasm", "**/*.map",
"/static/**", "/assets/**",
"/manifest.json", "/sw.js", "/robots.txt", "/sitemap.xml",
]

Full Preset

presets/react-spa.toml
[render]
wait_for = "networkidle"
timeout_secs = 15

[render.postprocess]
enabled = true
strip_scripts = true
strip_noscript = true
strip_comments = true
strip_event_handlers = true
strip_hydration_attrs = true
resolve_lazy_images = true

[render.content_validation]
enabled = true
min_text_length = 100
require_title = true
min_html_bytes = 1024

[routes]
include = ["/**"]
exclude = [
"/api/**", "/graphql",
"**/*.js", "**/*.css", "**/*.json", "**/*.xml",
"**/*.png", "**/*.jpg", "**/*.gif", "**/*.svg", "**/*.ico",
"**/*.woff", "**/*.woff2", "**/*.ttf", "**/*.wasm", "**/*.map",
"/static/**", "/assets/**",
"/manifest.json", "/sw.js", "/robots.txt", "/sitemap.xml",
]