Static rendering
Aplos can pre-render pages to static HTML at build time. Add a directive at the top of any page and it becomes static — instant first paint, full SEO, deployable to any static host.
Opt a page in
// src/pages/about.tsx "use static"; export default function About() { return <h1>About</h1>; }
When you run aplos build, Aplos emits dist/about.html containing the fully rendered HTML. Crawlers see the content. The browser displays it before any JavaScript runs.
Build with static rendering
Every aplos build pre-renders the pages that carry "use static", along with the URLs listed in paths. No flag is needed.
To pre-render every route instead, directive or not, pass --static:
aplos build --static
Routes with parameters (/blog/:slug) are still only pre-rendered through paths.
Output:
Pre-rendering 3 route(s)...
✓ / → dist/index.html
✓ /about → dist/about.html
✓ /contact → dist/contact.html
Mix static and dynamic
Pages without "use static" are still served as a SPA. You can mix freely:
src/pages/
index.tsx # "use static" → /
about.tsx # "use static" → /about
dashboard.tsx # SPA → /dashboard (client-rendered)
This is the SPA-first, static-on-opt-in model: keep dynamic logic where you need it, pre-render the parts that don't change per user.
Pre-render dynamic routes
To pre-render a route like /blog/:slug, list the URLs to generate at build time with the paths option in aplos.config.js. source is the page's file path pattern, and each entry in paths is a full URL:
// aplos.config.js export default { routes: [ { source: '/blog/[slug]', paths: ['/blog/hello-world', '/blog/second-post'], }, { source: '/products/[id]', paths: () => loadProductIds().map((id) => `/products/${id}`), }, ], };
paths is an array or a function returning one. The function is called synchronously, so read the data it needs from disk rather than over the network.
At build time, each URL is pre-rendered into its own static HTML file:
dist/blog/hello-world.html
dist/blog/second-post.html
dist/products/42.html
dist/products/43.html
A dynamic page that carries "use static" must be expanded this way: there is no URL to render for /blog/:slug itself, so the build fails until paths lists some. Likewise, a static page that throws while rendering fails the build instead of being left out of dist/.
Per-route metadata
Each static page can export a meta object to set its <title>, description,
canonical URL, Open Graph and Twitter Card tags. The values are extracted at
build time and injected directly into the pre-rendered HTML.
// src/pages/about.tsx "use static"; export const meta = { title: "About — Acme", description: "Learn about Acme.", og: { type: "website", image: "/og.png" }, }; export default function About() { return <h1>About</h1>; }
See the full reference in Per-route metadata.
When to use static rendering
Good fits:
- Marketing pages, blogs, documentation
- Product catalogs with known SKUs at build time
- Anything indexable by search engines
Not a good fit:
- Pages that depend on the logged-in user
- Pages whose content changes per request
For those, leave them as SPA pages — they will be client-rendered as before.
How it works
When you run aplos build, Aplos:
- Builds a server-side rendering bundle targeting Node (Rspack with
target: 'node'). - Collects the routes marked as static (via
"use static"or matching apathsconfig). - Executes the SSR bundle in Node and renders each route to HTML.
- Writes the resulting HTML next to your client-side bundles.
Dynamic routes that are not in the static set fall back to the SPA shell exactly as before — nothing changes for them.