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:

  1. Builds a server-side rendering bundle targeting Node (Rspack with target: 'node').
  2. Collects the routes marked as static (via "use static" or matching a paths config).
  3. Executes the SSR bundle in Node and renders each route to HTML.
  4. 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.