Skip to content

stackboxcms/cms

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@stackbox/cms

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.

Why this exists

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.

Requirements

  • Node.js >= 20

Installation

npm install @stackbox/cms

Concepts

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.

Project layout

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)

Quick start

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.

Blog module

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],
});

Bundled modules

import { createBlog } from "@stackbox/cms/modules/blog";

Development

npm run build      # compile the package
npm run typecheck  # type-check without emitting
npm test           # build, then run the test suite

License

BSD-3-Clause

About

CMS built for AI agents

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors