Motivation
Today, Primate’s build pipeline targets a monolithic server bundle (e.g. a single server.js) that powers SSR + hydration. Client navigation uses Primate’s existing fetch-browsing model: clicking from /docs to /docs/start typically fetches the next page’s components (often the same) and new data (often different).
This proposal introduces a new build target:
primate build --target=static
With --target=static, Primate produces a static site output:
- A full HTML file per route (SSG)
- Optional per-route page data to enable SPA-like client navigation
- Optional offline support via a Service Worker precache
Primary goals:
- Enable static hosting (CDN / object storage) with no server runtime
- Make frameworks like Svelte “pure frontend” in this target: no SSR server at runtime
- Preserve a fast UX by keeping Primate’s client navigation model (but backed by static artifacts)
Non-goals:
- Replace the existing monolithic target.
--target=static is additive.
- Define a universal “public env” mechanism (out of scope).
- Implement dynamic runtime rendering in the static target.
Proposed Solution
1) Add --target=static to primate build
Add a new build target that generates:
- Static HTML per route (SSG output)
- Static page-data per route (for client navigation)
- A manifest that maps routes to generated artifacts (for runtime + tooling)
- (Optional) A Service Worker for offline caching
Example routes:
Static output:
build/
docs/
index.html
docs/
start/
index.html
page-data/
docs.json
docs-start.json
client/
app.<hash>.js
app.<hash>.css
primate-manifest.json
Note: a “flattened naming” (docs.html, docs-start.html) can be supported as a config option, but defaulting to “folder + index.html” yields clean URLs on common static hosts.
2) Static HTML generation per route
For each route, Primate renders a full HTML document at build time.
Inputs:
- Route list (from router config + optional crawling)
- The same page composition logic used today (components + data)
- Optional preloaded data and route params
Output:
- An HTML document that can be served directly by any static host
Key behavior:
- The static HTML should work with:
- No JS (content visible; links work)
- JS enabled (client runtime boots and enhances navigation)
3) Per-route page-data (/page-data/<route>.json) for SPA-like navigation
Generate one JSON per route.
Example:
/page-data/docs.json
/page-data/docs-start.json
Each JSON file contains the minimum information needed for Primate’s client runtime to “transition” to the route without a full page reload.
Suggested shape:
{
"route": "/docs/start",
"rev": "build-hash-or-timestamp",
"components": [
{ "id": "StaticPage", "chunk": "/assets/chunks/static-page.<hash>.js" } // chunks aren't currently supported by Primate
],
"data": {
"html": "<h1>Getting Started</h1>...",
"navbar": { "items": [/* ... */] }
},
"head": {
"title": "Docs · Getting Started",
"meta": [/* ... */]
}
}
Client navigation behavior (default):
- On click, fetch the next route’s page-data JSON
- Reuse already-loaded component chunks
- Apply
data, update <head> (title/meta), and update the view
- Update
history.pushState
Fallback behavior:
- If
page-data is missing or fails to load, fall back to fetching the route’s HTML and:
- Either do a full navigation, or
- Extract embedded page-data from the HTML (see section 4)
4) Embed initial page-data inside each HTML
To avoid an extra request on initial load, embed each page’s page-data inside the generated HTML:
<script type="application/json" id="__PRIMATE_PAGE__">
{"route":"/docs/start","rev":"...","components":[...],"data":{...},"head":{...}}
</script>
Benefits
- Zero extra network requests on initial load
- Keeps HTML as a single canonical artifact for each route
- Enables an optional “HTML fetch on navigation” strategy if desired
Runtime boot
- On initial load, the client runtime reads
#__PRIMATE_PAGE__
- Initializes router state + component registry + data store ("mini-hydration")
5) Optional: Prefetch + caching
To preserve fast navigation:
- Prefetch page-data on:
- link hover/focus
- or when links enter the viewport (IntersectionObserver)
- Cache page-data in memory and/or Cache Storage
Suggested runtime hooks:
primate.prefetch("/docs/start"); // fetches /page-data/docs-start.json
primate.navigate("/docs/start"); // uses cached data if available
6) Offline support (opt-in)
Provide an opt-in offline mode that precaches:
- all route HTML files
- all route page-data files
- app JS/CSS/assets
CLI option:
primate build --target=static --static.offline
This generates:
dist/sw.js
dist/sw-manifest.json (precache list with hashes)
With offline enabled:
- Client navigations continue to fetch page-data, but requests resolve from Cache Storage when offline.
7) Manifest: primate-manifest.json
Generate a manifest mapping routes to artifacts and build metadata.
Example:
{
"target": "static",
"rev": "2026-03-01T12:34:56Z",
"routes": {
"/docs": {
"html": "/docs/index.html",
"data": "/page-data/docs.json"
},
"/docs/start": {
"html": "/docs/start/index.html",
"data": "/page-data/docs-start.json"
}
},
"assets": {
"entry": "/assets/app.<hash>.js"
}
}
Uses:
- Runtime can resolve
route -> data url
- Tooling can validate completeness
- Deploy tooling can diff route-level changes
Configuration / CLI surface
CLI
primate build --target=static
--static.offline (optional)
--static.layout=folders|flat (optional)
folders → /docs/start/index.html (default)
flat → docs-start.html for legacy/simple hosting environments
--static.navigation=page-data|html|auto (optional)
page-data → fetch /page-data/*.json on nav (default)
html → fetch *.html and extract embedded __PRIMATE_PAGE__
auto → try page-data, fallback to html
Framework behavior (Svelte example)
In --target=static, Svelte becomes client-only:
- Static HTML contains rendered markup at build time
- Svelte runtime hydrates/enhances the existing DOM
- Navigations update app state using page-data and re-render client-side
This keeps “SSG + SPA transitions”:
- Direct loads: SEO-friendly, fast initial render
- Subsequent navigations: snappy, data-driven, minimal reloads
Backward compatibility
- Existing targets remain unchanged.
- Static target is additive and opt-in.
- Runtime navigation defaults should preserve existing mental model:
- “navigate → fetch structured data → update UI”
- but data now comes from static
page-data/*.json
Security
Static output is public by definition:
Summary of changes
| Area |
Change |
| Build CLI |
Add --target=static |
| Build pipeline |
Render full HTML per route |
| Output |
Emit per-route page-data/*.json |
| Output |
Emit primate-manifest.json |
| Runtime |
Support client navigation using static page-data |
| Runtime |
Prefetch + caching hooks (optional) |
| Offline (opt-in) |
Generate SW + precache manifest |
Implementation sketch
Step A — Route enumeration
- Collect known routes from router definitions
- Optional “crawler mode” for discovering dynamic routes via a user-provided list (e.g. docs slugs)
Step B — Build-time render
For each route:
- Produce:
html (full document)
page-data (structured payload)
- Embed
page-data inside HTML (__PRIMATE_PAGE__)
Step C — Runtime navigation adapter
- In static target builds, runtime reads
primate-manifest.json
- Route → page-data URL resolution
- Navigation uses:
- fetch JSON by default
- fallback to HTML extraction
Step D — Offline (optional)
- Generate SW script + manifest containing hashed assets + per-route files
- Precache during install
Open questions
- File naming default
- Default
folders (/docs/start/index.html) vs flat (docs-start.html)
- Navigation fetch strategy
- Fetch JSON by default, or fetch HTML + extract data?
- Head management
- Should page-data carry a declarative
head object, or do we re-run a head-renderer client-side?
- Dynamic routes
- How do we generate paths for param routes (e.g.
/blog/[slug])? Likely requires user-provided route inventory, or scanning.
- Error handling
- What is the runtime fallback when page-data is missing (404)? Should it fetch HTML or show a client-side 404?
- Chunk/component mapping
- Do we store a
components[] list in page-data, or rely on the client bundle to already include all needed components?
- Data size
- How to avoid duplicating large shared data (e.g. nav tree) across many page-data files? (Possible future: shared data chunks.)
- Offline scope
- Offline by default vs opt-in, and should offline support allow “partial” offline (specific route groups)?
- Build determinism
- Ensure stable hashes when content is unchanged to maximize CDN caching.
Acceptance criteria
primate build --target=static produces a build/ that can be served by a static host
- Each route is accessible via direct load and displays correct content
- Client navigation between two routes:
- does not require a full page reload
- fetches per-route page-data by default
primate-manifest.json exists and correctly maps all routes
- (If
--static.offline) the site remains navigable between prebuilt routes without network access
Motivation
Today, Primate’s build pipeline targets a monolithic server bundle (e.g. a single
server.js) that powers SSR + hydration. Client navigation uses Primate’s existing fetch-browsing model: clicking from/docsto/docs/starttypically fetches the next page’s components (often the same) and new data (often different).This proposal introduces a new build target:
With
--target=static, Primate produces a static site output:Primary goals:
Non-goals:
--target=staticis additive.Proposed Solution
1) Add
--target=statictoprimate buildAdd a new build target that generates:
Example routes:
/docs/docs/startStatic output:
2) Static HTML generation per route
For each route, Primate renders a full HTML document at build time.
Inputs:
Output:
Key behavior:
3) Per-route page-data (
/page-data/<route>.json) for SPA-like navigationGenerate one JSON per route.
Example:
/page-data/docs.json/page-data/docs-start.jsonEach JSON file contains the minimum information needed for Primate’s client runtime to “transition” to the route without a full page reload.
Suggested shape:
Client navigation behavior (default):
data, update<head>(title/meta), and update the viewhistory.pushStateFallback behavior:
page-datais missing or fails to load, fall back to fetching the route’s HTML and:4) Embed initial page-data inside each HTML
To avoid an extra request on initial load, embed each page’s page-data inside the generated HTML:
Benefits
Runtime boot
#__PRIMATE_PAGE__5) Optional: Prefetch + caching
To preserve fast navigation:
Suggested runtime hooks:
6) Offline support (opt-in)
Provide an opt-in offline mode that precaches:
CLI option:
This generates:
dist/sw.jsdist/sw-manifest.json(precache list with hashes)With offline enabled:
7) Manifest:
primate-manifest.jsonGenerate a manifest mapping routes to artifacts and build metadata.
Example:
{ "target": "static", "rev": "2026-03-01T12:34:56Z", "routes": { "/docs": { "html": "/docs/index.html", "data": "/page-data/docs.json" }, "/docs/start": { "html": "/docs/start/index.html", "data": "/page-data/docs-start.json" } }, "assets": { "entry": "/assets/app.<hash>.js" } }Uses:
route -> data urlConfiguration / CLI surface
CLI
primate build --target=static--static.offline(optional)--static.layout=folders|flat(optional)folders→/docs/start/index.html(default)flat→docs-start.htmlfor legacy/simple hosting environments--static.navigation=page-data|html|auto(optional)page-data→ fetch/page-data/*.jsonon nav (default)html→ fetch*.htmland extract embedded__PRIMATE_PAGE__auto→ try page-data, fallback to htmlFramework behavior (Svelte example)
In
--target=static, Svelte becomes client-only:This keeps “SSG + SPA transitions”:
Backward compatibility
page-data/*.jsonSecurity
Static output is public by definition:
AppFacade#env(key: string)#251)--target=staticoutput can be inspected by end usersSummary of changes
--target=staticpage-data/*.jsonprimate-manifest.jsonImplementation sketch
Step A — Route enumeration
Step B — Build-time render
For each route:
html(full document)page-data(structured payload)page-datainside HTML (__PRIMATE_PAGE__)Step C — Runtime navigation adapter
primate-manifest.jsonStep D — Offline (optional)
Open questions
folders(/docs/start/index.html) vsflat(docs-start.html)headobject, or do we re-run a head-renderer client-side?/blog/[slug])? Likely requires user-provided route inventory, or scanning.components[]list in page-data, or rely on the client bundle to already include all needed components?Acceptance criteria
primate build --target=staticproduces abuild/that can be served by a static hostprimate-manifest.jsonexists and correctly maps all routes--static.offline) the site remains navigable between prebuilt routes without network access