prestruct docs

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:

  1. Install the CLI (brings the engine, no more hand-synced copies):
    npm install -D prestruct
    npx prestruct init
    

    Files that were yours (ssr.config.js adjustments, AppLayout.jsx, routes) are never clobbered – init prints skip for files that exist.

  2. What changed underfoot (v0.2.x -> current):

    Old habit Current reality
    Manually copied + diverged scripts/*.js between projects Single source engine/, generated byte-identically to init/ + example/ by scripts/sync-engine.mjs
    npm run build = 3 chained node scripts npx prestruct build (or keep your 3-step script – it still works)
    Broken prerenders exited 0 (a Done. 0/2 pages rendered build 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=1 still turns those fatal too
    Example app has bespoke feature forks (deployIdFooter) Config flags on the shared engine (deployIdFooter: true is opt-in)
    Static public/robots.txt only Optional 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. When proxy.url is set AND botList is non-empty, inject-brand syncs it into scripts/proxy.worker.js (a bare config.proxy.botList alone 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).
  1. Rebuild with a fresh incremental cache:
    npx prestruct build --clean
    

    Old caches only know the routes they rendered; a --clean build gives the new engine a blank slate. Use --force thereafter when configs change without touching route paths.

  2. Validate before you commit: npx prestruct validate prints 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

  1. Add Vite
    npm install vite @vitejs/plugin-react
    
  2. Create vite.config.js
    import { defineConfig } from 'vite'
    import react from '@vitejs/plugin-react'
    
    export default defineConfig({
      plugins: [react()],
      build: { outDir: 'dist' }
    })
    
  3. Add prestruct files
    npm install -D prestruct
    npx prestruct init          # scaffolds scripts/, src/, ssr.config.example.js
    cp ssr.config.example.js ssr.config.js
    

    Or copy the same files by hand if you prefer no npm dep: copy scripts/inject-brand.js and scripts/prerender.js, create ssr.config.js and AppLayout.jsx (no BrowserRouter!).

  4. 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.

  5. 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

  1. Remove Next.js
    npm uninstall next react
    
  2. Add Vite + React
    npm install react react-dom react-router-dom
    npm install -D vite @vitejs/plugin-react
    
  3. 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
    ]
    
  4. 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"
    />
    
  5. 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

  1. Remove Gatsby
    npm uninstall gatsby react gatsby-plugin-*
    
  2. Install Vite + React
    npm install react react-dom react-router-dom
    npm install -D vite @vitejs/plugin-react
    
  3. 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 }
      }))
    ]
    
  4. 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

  1. Remove React Static
    npm uninstall react-static
    
  2. 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

  1. Add React to Astro (or convert to full React)

    If keeping Astro with React:

    • Add prestruct as 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
    
  2. Migrate Components

    ---
    // Astro component
    const { title } = Astro.props
    ---
    <h1>{title}</h1>
    
    <!-- React component -->
    export default function Heading({ title }) {
      return <h1>{title}</h1>
    }
    
  3. 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:


After Migration

  1. Test locally: npx vite preview serves dist/ at :4173
  2. Validate the config: npx prestruct validate (catches mistakes before CI does)
  3. Check SEO: Verify meta tags, sitemap, canonical URLs, robots.txt
  4. Watch the cache stats snapshot: .prestruct/status.json (project root, not dist) reports the last build’s route render status
  5. Lighthouse audit: Should score 90+ on all metrics

Next steps