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:
| Attribute | Purpose in React | Why It's Stripped |
|---|---|---|
data-reactroot | Marks the root element | Only needed for client hydration |
data-reactid | Legacy React 15 element IDs | Only needed for client reconciliation |
data-react-checksum | Legacy SSR checksum | Only needed for hydration validation |
data-react-helmet | Marks Helmet-managed elements | Only 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
[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",
]