A small, code-first CMS engine for building dynamic Cloudflare Worker sites. Stackbox is designed to be driven by AI: pages, templates, and content modules are plain TypeScript files with typed, composable APIs, so an agent can author and assemble a site without a database, admin UI, or hand-written backend.
Pages are rendered on each request inside a Cloudflare Worker, so content, templates, and modules can be fully dynamic — driven by request data, bindings (KV, D1, R2), and async data fetching.
Traditional CMSes assume a human clicking around an admin panel. Stackbox inverts that: a site is TypeScript modules assembled into a Worker. Every primitive (createSiteConfig, createSite, createTemplate, createPage, createModule) is a typed factory suited for AI to generate, edit, and validate site content as code — and the same files render dynamically on Cloudflare Workers at request time.
- Node.js >= 20
npm install @stackbox/cms| Primitive | Factory | Purpose |
|---|---|---|
| Site config | createSiteConfig(config) |
Definition-time settings shared by templates and pages. |
| Site | createSite(siteConfig, { pages } |
Runtime router with fetch(request, env) for Cloudflare Workers. |
| Template | createTemplate({ siteConfig, slots, render } |
A reusable page layout that declares named slots. |
| Page | createPage(template, { path, title, slots } |
A single URL, built by filling a template's slots with content. |
| Module | createModule({ name, render } |
A self-contained content block placed into a slot. |
Slots are named regions in a template. Page content — strings, HTML, or modules — is dropped into slots, and the engine resolves and renders everything (including async modules, concurrently) to a single HTML string.
my-worker/
site.config.ts # createSiteConfig({ name, url, ... })
worker.ts # createSite(siteConfig, { pages }) — default export for Cloudflare
templates/
site-template.ts # shared createTemplate() layouts
pages/
home.ts # exports homePage
blog.ts # createBlog() + createPage() for listing and posts
content/blog/ # markdown posts (read at bundle time)
site.config.ts:
import { createSiteConfig } from "@stackbox/cms";
export default createSiteConfig({
name: "My Site",
url: "https://example.com",
});templates/site-template.ts:
import { createTemplate, html } from "@stackbox/cms";
import siteConfig from "../site.config";
export const siteTemplate = createTemplate({
siteConfig,
slots: [{ name: "content", options: { required: true, primary: true } }],
render({ slots }) {
return html`<main>${slots.content.render()}</main>`;
},
});pages/home.ts:
import { createPage } from "@stackbox/cms";
import { siteTemplate } from "../templates/site-template";
const homePage = createPage(siteTemplate, {
path: "/",
title: "Home",
slots: { content: ["<p>Welcome to my site.</p>"] },
});
export default homePage;worker.ts:
import { createSite } from "@stackbox/cms";
import siteConfig from "./site.config";
import homePage from "./pages/home";
import aboutPage from "./pages/about";
export default createSite(siteConfig, {
pages: [homePage, aboutPage],
});Deploy with wrangler. The default export's fetch(request, env) handles each request.
createBlog() loads markdown at bundle time and returns content objects you wire into your own pages with createPage() — so you control templates, slots, and any extra content alongside blog output.
// pages/blog.ts
import { join } from "node:path";
import { createPage } from "@stackbox/cms";
import { createBlog } from "@stackbox/cms/modules/blog";
import { siteTemplate } from "../templates/site-template";
const blog = createBlog({
contentPath: join(import.meta.dirname, "../content/blog"),
pathPrefix: "/blog",
postsPerPage: 10, // optional — omit to put all posts on one listing page
});
export const blogListingPages = blog.listings.map((listing, index) =>
createPage(siteTemplate, {
path: listing.path,
title: index === 0 ? "Blog" : `Blog — page ${index + 1}`,
slots: {
content: [...listing.content, "<p>Subscribe for updates</p>"],
},
}),
);
export const blogPostPages = blog.posts.map((post) =>
createPage(siteTemplate, {
path: post.path,
title: post.title,
meta: post.meta,
slots: { content: [...post.content] },
}),
);// worker.ts
import { blogListingPages, blogPostPages } from "./pages/blog";
export default createSite(siteConfig, {
pages: [homePage, ...blogListingPages, ...blogPostPages],
});import { createBlog } from "@stackbox/cms/modules/blog";npm run build # compile the package
npm run typecheck # type-check without emitting
npm test # build, then run the test suiteBSD-3-Clause