Moving to prestruct from other solutions.
From prestruct v0.1 / v0.2 (older layouts)
If your project predates the npm package, the fastest path is a clean scaffold + copy forward:
- Install the CLI (brings the engine, no more hand-synced copies):
npm install -D prestruct npx prestruct initFiles that were yours (ssr.config.js adjustments, AppLayout.jsx, routes) are never clobbered – init prints
skipfor files that exist. -
What changed underfoot (v0.2.x -> current):
Old habit Current reality Manually copied + diverged scripts/*.jsbetween projectsSingle source engine/, generated byte-identically toinit/+example/byscripts/sync-engine.mjsnpm run build= 3 chained node scriptsnpx prestruct build(or keep your 3-step script – it still works)Broken prerenders exited 0 (a Done. 0/2 pages renderedbuild deployed empty routes silently)Render failures exit 1 unconditionally, in both the CLI and the direct engine script, naming every failed route. Config diagnostics remain non-fatal by default; PRESTRUCT_STRICT=1still turns those fatal tooExample app has bespoke feature forks ( deployIdFooter)Config flags on the shared engine ( deployIdFooter: trueis opt-in)Static public/robots.txtonlyOptional build-time robots.txt via config.robots(see docs/guides/config.md)Bot list maintained in two places (VPS proxy + Worker proxy) One place: config.proxy.botList. Whenproxy.urlis set ANDbotListis non-empty, inject-brand syncs it intoscripts/proxy.worker.js(a bareconfig.proxy.botListalone does nothing; the sync is skipped if the Worker file is absent)
What changed in v1.0.0 (behavioral deltas and what to do)
If your build has been green-but-broken, expect changes – every delta below makes a failure visible that used to ride through:
| v1.0.0 change | Action |
|---|---|
Prerender failures fail the build. Old builds exited 0 even when every route errored (Home is not defined per route, then deploy). Now both prestruct build and node scripts/prerender.js exit 1 and list each failed path. |
If a CI build starts failing on upgrade, it was already broken – read the listed route errors (usually a <Route> wired to a page whose import is still commented out or missing) and fix them. There is no opt-out; exit 0 on a broken deploy was the bug. |
Empty route lists are invalid. routes: [] exits validation with routes is empty -- configure at least one route. Subroutes-only lists stay valid (vite still ships index.html). |
Configure at least one route, or delete the empty config. |
Duplicate route paths are invalid. Two routes with the same path exited 0 and wrote the same dist file twice with different meta. Now: exit 1, diagnosed by index. |
Dedupe – keep the entry whose meta you want. |
fetchRoutes() actually runs. The CMS hook was documented but did nothing. Now it merges at build time: dedupe by path, static entry wins; a nullish result degrades to the static list. |
If you relied on the hook silently doing nothing, expect CMS routes to appear. Wire the returned routes to JSX in your AppLayout the same way you wired static ones, or the prerender fails loudly naming the route. |
Bot detection is case-insensitive. Tokens were matched against a lowercased UA but kept their own case (Googlebot never matched googlebot) – every configured bot got a 302. |
No config change needed; mixed-case botList tokens now work as documented. |
The proxy runs on Express 5. app.all('*')-style wildcards crashed Express 5 at startup (path-to-regexp rejects *). The proxy now uses a pathless app.use middleware – identical behavior on Express 4 and 5. |
Nothing to migrate; custom forks that re-register wildcards need the same fix. The proxy’s Node floor is 20 (proxy.package.json engines). |
Cache-refresh auth is env-only. PRESTRUCT_SECRET is the only correct place (a config proxy.secret was never read). Unset secret = refresh feature off, never allow-all. |
Remove any config-side secret attempts; set the env var where the proxy runs (wrangler secret put on Workers). |
- Rebuild with a fresh incremental cache:
npx prestruct build --cleanOld caches only know the routes they rendered; a
--cleanbuild gives the new engine a blank slate. Use--forcethereafter when configs change without touching route paths. - Validate before you commit:
npx prestruct validateprints every diagnostic line-by-line, so config mistakes are either visible (default) or fatal (PRESTRUCT_STRICT=1).
From Create React App (CRA)
What Changes
| CRA | Prestruct |
|---|---|
react-scripts build |
npm run build (3-step) |
| Client-only | Prerendered static HTML |
| No SEO meta handling | Automatic meta injection |
| SPA fallback needed | Per-route HTML files |
Steps
- Add Vite
npm install vite @vitejs/plugin-react - Create vite.config.js
import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], build: { outDir: 'dist' } }) - Add prestruct files
npm install -D prestruct npx prestruct init # scaffolds scripts/, src/, ssr.config.example.js cp ssr.config.example.js ssr.config.jsOr copy the same files by hand if you prefer no npm dep: copy
scripts/inject-brand.jsandscripts/prerender.js, createssr.config.jsandAppLayout.jsx(no BrowserRouter!). - Build
{ "scripts": { "dev": "vite", "build": "prestruct build", "preview": "vite preview" } }prestruct build= validate ->vite build-> inject meta -> prerender. The equivalent 3-step script (vite build && node scripts/inject-brand.js && node scripts/prerender.js) still works if you’d rather stay dep-free. - Deploy to Cloudflare Pages instead of static hosting
From Next.js Static Export
What Changes
| Next.js | Prestruct |
|---|---|
next build && next export |
npm run build |
_next/ folder |
assets/ (hashed) |
getStaticProps |
Build-time data in ssr.config.js |
| Image Optimization API | Use <img srcset> |
| API Routes | Cloudflare Pages Functions |
Steps
- Remove Next.js
npm uninstall next react - Add Vite + React
npm install react react-dom react-router-dom npm install -D vite @vitejs/plugin-react -
Adapt Pages
// Next.js page export async function getStaticProps() { const data = await fetchData() return { props: { data } } } // Prestruct - move to ssr.config.js or build script const routes = [ { path: '/', meta: {...} }, // ... dynamic routes generated at build time ] -
Update Image Usage
// Next.js <Image src="/img.png" width={800} height={600} /> // Prestruct <img src="/img.png" srcset="/img-400.png 400w, /img-800.png 800w" sizes="(max-width: 600px) 400px, 800px" width={800} height={600} loading="lazy" /> - Remove Next.js config, add Vite config
From Gatsby
What Changes
| Gatsby | Prestruct |
|---|---|
| GraphQL data layer | Static config / build scripts |
| gatsby-node.js | ssr.config.js routes |
| Head API | usePageMeta hook |
| Image Plugin | Manual srcset |
Steps
- Remove Gatsby
npm uninstall gatsby react gatsby-plugin-* - Install Vite + React
npm install react react-dom react-router-dom npm install -D vite @vitejs/plugin-react -
Migrate data
// gatsby-node.js exports.createPages = async ({ actions }) => { const posts = await getPosts() posts.forEach(post => { actions.createPage({ path: `/blog/${post.slug}`, component: './src/templates/post.jsx', context: { post } }) }) } // ssr.config.js const posts = await getPosts() // Call at build time routes: [ ...posts.map(post => ({ path: `/blog/${post.slug}/`, meta: { title: post.title } })) ] -
Update image handling
Gatsby’s image processing is removed. Use standard
<img>with srcset or a Vite plugin.
From React Static (Babel)
What Changes
| React Static | Prestruct |
|---|---|
react-static build |
npm run build |
getRoutes |
ssr.config.js routes |
getSiteData |
Build-time fetch in ssr.config.js |
Steps
- Remove React Static
npm uninstall react-static -
Migrate routes
// react-static.config.js export default { getRoutes: async () => { const posts = await fetch('/api/posts').then(r => r.json()) return [ { path: '/', component: 'src/pages/Home.jsx' }, ...posts.map(post => ({ path: `/blog/${post.slug}`, component: 'src/pages/BlogPost.jsx', getData: () => ({ post }) })) ] } } // ssr.config.js const posts = await fetch('/api/posts').then(r => r.json()) export default { routes: [ { path: '/', meta: {...} }, ...posts.map(post => ({ path: `/blog/${post.slug}/`, meta: { title: post.title } })) ] }
From Astro
What Changes
| Astro | Prestruct |
|---|---|
.astro files |
.jsx React components |
| Frontmatter | React hooks |
| Collection API | Build-time fetch |
| Islands (Astro) | Islands (prestruct) |
Steps
-
Add React to Astro (or convert to full React)
If keeping Astro with React:
- Add
prestructas prerendering layer
If converting to pure React:
npm uninstall astro @astrojs/react npm install react react-dom react-router-dom npm install -D vite @vitejs/plugin-react - Add
-
Migrate Components
--- // Astro component const { title } = Astro.props --- <h1>{title}</h1> <!-- React component --> export default function Heading({ title }) { return <h1>{title}</h1> } -
Migrate Data Fetching
--- // In Astro frontmatter const data = await fetch('https://api.example.com').then(r => r.json()) --- <!-- In React - do this in ssr.config.js or build script -->
Common Migration Patterns
Dynamic Data at Build Time
Prefer the fetchRoutes() hook over hand-rolled list building inside
ssr.config.js; it keeps static routes separate from CMS-sourced ones and
dedupes by path (static wins):
// ssr.config.js
export default {
siteUrl: 'https://example.com',
async fetchRoutes() {
const posts = await fetch('https://cms.example.com/api/posts')
.then(r => r.json())
return posts.map(post => ({
path: `/blog/${post.slug}/`,
priority: '0.6',
meta: { title: post.title, description: post.excerpt },
}))
},
// routes: [ ...static routes stay here, unaffected... ]
}
Environment-Based Config
// ssr.config.js
const isProd = process.env.NODE_ENV === 'production'
export default {
siteUrl: isProd
? 'https://example.com'
: 'http://localhost:5173',
// ...
}
Shared Components
Most UI libraries work with prestruct. Test for:
- SSR compatibility (no window/document at render time)
- No inline styles causing hydration mismatch
After Migration
- Test locally:
npx vite previewservesdist/at :4173 - Validate the config:
npx prestruct validate(catches mistakes before CI does) - Check SEO: Verify meta tags, sitemap, canonical URLs, robots.txt
- Watch the cache stats snapshot:
.prestruct/status.json(project root, not dist) reports the last build’s route render status - Lighthouse audit: Should score 90+ on all metrics
Next steps
- Getting Started - the full walkthrough for a clean start
- Examples - target project shapes