@moku-labs/web
Content static-site generator and SPA framework. Same render at build and in the browser.
What it is
@moku-labs/web renders Preact pages to static HTML for search engines and first paint. Then it
hydrates islands and, if you want it, navigates on the client from JSON. The same render
function runs at build and in the browser, so the two can never disagree.
It is a Moku framework: the @moku-labs/core kernel plus a list of plugins. You write one
createApp call. The browser-safe plugins are always there. You add the Node-only plugins for
the build.
Five things it does
- SSG and SPA parity. One route, two runtimes. The build writes
HTML, the browser re-runs the same
renderwith the saved data. - A typed route.
route("/{slug}/").load().render().head(). Params come from the pattern,ctx.datafromload. - Full SEO. Title template, canonical, Open Graph, Twitter, JSON-LD, hreflang, feeds, sitemap and OG images.
- A node-free browser entry.
@moku-labs/web/browserhas zero Node imports in its graph. About 50 kB gzip. - Islands. Only the interactive parts run in the browser, each with its own state, render, events and API.
Quick start
- Install the framework and its peers:
bun add @moku-labs/web @moku-labs/core @moku-labs/common preact preact-render-to-string. - Write the routes with
route(): a pattern, aload, arender, ahead. - Compose the app with
createApp, addingcontentPluginandbuildPluginfor the build. - Build with
await app.build.run(). The site is indist/.
A whole static blog fits in two files:
import { defineRoutes, route } from "@moku-labs/web/browser";
export const routes = defineRoutes({
home: route("/")
.render(() => <h1>My Blog</h1>)
.head(() => ({ title: "My Blog" })),
about: route("/about/")
.render(() => <p>Notes on the web.</p>)
.head(() => ({ title: "About" }))
});.head()feeds the title template, canonical and OG tags.
The quick start goes from an empty folder to a deployed site.
Two entry points
| Entry | Format | Use it for |
|---|---|---|
@moku-labs/web | ESM and CJS | The Node build. Everything, including buildPlugin, deployPlugin, cliPlugin, fileSystemContent and the Node env providers. |
@moku-labs/web/browser | ESM only | The client bundle. The same createApp over the same defaults, plus dataPlugin, the route helpers, createIsland and the head helpers. env reads browserEnv() with no config. |
The eleven plugins
The defaults are loaded by every createApp. You can only configure them. The others you add in
plugins: [...].
| Plugin | Kind | What it does |
|---|---|---|
| site | default | Site name, URL, author, description, canonical URLs. |
| i18n | default | Locales, default locale, translations, og:locale map. |
| router | default | The route table, the matcher, URL and file paths, the render mode. |
| head | default | The <head> of every page. |
| spa | default | Islands, intercepted navigation, progress bar. Inert on Node. |
| content | Node build | Markdown articles to HTML, with locale fallback. |
| collection | optional | (collection, shard) → JSON file, read on demand. |
| data | optional | Page path → JSON file, for client navigation. |
| build | Node-only | The static build into dist/. |
| deploy | Node-only | Cloudflare Pages through wrangler. |
| cli | Node-only | build, serve, preview, deploy with the branded panel. |
Two core plugins come with every app: log and env, from @moku-labs/common.
Start with the Claude plugin
Install the moku plugin in Claude Code and describe the site in plain words. It knows this framework, writes the routes, the islands and the tests, and runs the build.
/plugin marketplace add moku-labs/claude /plugin install moku@moku-labs
Build a blog with tags and an RSS feed on @moku-labs/web
Add a search box that reads a JSON index built at deploy time
Where to next
- Quick start: a static blog in two files, client navigation in a third.
- How it works: one route, two places.
- Routing:
route().load().render().head(), the single contract. - Islands: the parts that run in the browser.
- API reference: every export of both entries.