@k8ordo/static

Get Started

Install @k8ordo/static, add one plugin to vite.config.ts, put a layout and a page under routes/, and vite build writes the site out as files. This page walks the shortest path there, and what choosing this mode means.

The mode is the dependency

Installing this package is what makes an application static. Request-time data is not a rule to remember here: there is no request, so there is nothing to read it from.

A Server Action is the one thing the underlying RSC pipeline would still compile, so the build refuses every module that declares 'use server' by name, rather than shipping a form that posts into nothing.

static build cannot ship Server Actions a file cannot receive one, and these declare 'use server':
  src/routes/_parts/guestbook.ts
this application wants @k8ordo/server

vite dev is a running server that would happily accept that POST, so the same refusal is raised there too, the moment the file is compiled — a form that works in development and posts into nothing in production is worse than one that never worked.

Choosing the other mode means installing @k8ordo/server instead, and nothing else about the application changes — the same route grammar, the same boundaries, the same request handler, called for each route at build time instead of per request. That is why the plugin is called framework() in both packages: the mode is the import, and vite.config.ts reads the same either way.

Install

The router and React are runtime dependencies. @k8ordo/static and Vite are only ever needed at build time, so they are development dependencies. server-only is there so TypeScript can resolve the import that marks a server-only module.

npm install @k8ordo/router react react-dom server-only
npm install -D @k8ordo/static vite

The peer dependencies, and the Node.js the build runs on.

  • @k8ordo/router
  • React >= 19.3.0
  • React DOM >= 19.3.0
  • Vite >= 8.2.1
  • Node.js >= 24

vite.config.ts

The plugin is framework(). It takes three options: routesDir, the route directory (default src/routes); paths, the pathnames for routes with parameters; and site, the origin the site is served from, which makes the build write sitemap.xml too.

// vite.config.ts
import { framework } from '@k8ordo/static';
import { defineConfig } from 'vite';

export default defineConfig({ plugins: [framework()] });

framework() returns an array of Vite plugins that already includes React's plugin (Fast Refresh) and the RSC pipeline, so there is no @vitejs/plugin-react to add yourself.

tsconfig.json

For the type wiring to apply, list .k8ordo in tsconfig.json's include with a glob. The name starts with a dot, and a bare directory entry silently skips it — the build still works, and href simply stops being checked against the table.

{
  "include": ["src/**/*.ts", "src/**/*.tsx", ".k8ordo/**/*.ts"]
}

The smallest routes/

The root layout renders <html> and <body>. The framework has no document template of its own, because a template you cannot see is a template you cannot change. A file with no directive is a Server Component.

// src/routes/layout.tsx
import type { ReactNode } from 'react';

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}
// src/routes/page.tsx
export default function HomePage() {
  return (
    <>
      <title>home</title>
      <h1>hello</h1>
    </>
  );
}

The root layout is the document

Hydration checks all of the document. Anything that rewrites the HTML between what the framework wrote and the browser — a CDN that inlines web fonts, obfuscates email addresses or defers scripts — changes a tree React is about to reconcile, and hydration fails on the difference. The way to be unaffected is to give such a service nothing to rewrite: serve the assets from the same origin rather than linking them from another one.

Run it

vite dev is a server that renders per request, Fast Refresh included. vite build renders every route into dist/client/, and that directory is the site.

vite dev
vite build

vite dev runs the same handler as the build but writes no files, so a few things differ from production. A param value paths does not list still renders. redirect.ts answers with a 307 (a 308 when permanent) rather than a redirect page. An unknown URL is answered by the handler under a 404 — with not-found.tsx, when there is one — rather than by the host's 404.html. A page whose Server Component throws answers a 500 carrying the thrown message rather than error.tsx — where the build would stop.

The build ends by reporting what it wrote, in one line.

k8ordo: wrote 1 routes

What gets generated

The first vite dev or vite build writes .k8ordo/: the route table (routes.gen.ts), the type wiring that ties it into @k8ordo/router (register.gen.ts), and a .gitignore that keeps the directory out of git.

vite build does not type-check; the satisfies checks report through tsc. .k8ordo/ is not in git, so on a fresh checkout run vite dev or vite build once before tsc to write it.

What is in them, and what the build refuses, is covered here. routes/

When to choose @k8ordo/server

Anything that needs the request is not in this mode. If the application needs any of the following, it wants @k8ordo/server — and everything on this page stays exactly as it is.

  • Server Actions a form can post to, and redirect() from them
  • A page reading the request's headers and cookies
  • Status codes the application decides, a real 404 for an unknown URL included
  • Parameter values that cannot be listed ahead of time

@k8ordo/server

Next steps