zumbrunn.com

Introducing Dune: A Flat-File CMS for Deno and Fresh

When I wrote about thirty years of Javascript on the server, the conclusion almost wrote itself: Deno and Fresh synthesize the lessons of three decades of serverjs and currently offer the best overall runtime and web framework for Javascript on the server. The question that conclusion left open was what to build with them. For me, the answer was the same thing it has been since the LiveWire days: a system for publishing content on the web. So I built one. It's called Dune, it's open source, and it runs the site you are reading this on.

Dune is a flat-file CMS. Content lives as files on disk — Markdown, MDX, or TSX — organized in ordered folders, configured with YAML frontmatter. There is no database to install, no build pipeline to configure, no node_modules directory to explain. You write a file, the server serves a page. From that starting point, it grows with your needs: themes with Preact templates, a full admin panel, authentication, plugins, a database layer when a project genuinely needs one. But the resting state is files in folders, and everything else is opt-in.


The Case for Flat Files

It may seem odd, after a career spent building database-backed publishing systems, to argue for keeping content out of the database. But the argument has been quietly winning for years.

A CMS database earns its complexity when content is genuinely relational and written concurrently by many hands. Most websites are neither. They are a few dozen to a few thousand documents, written by a few people, read by many. For that shape of problem, the filesystem is a remarkably good database: it has hierarchical structure, atomic writes, timestamps, and an access control model. Combined with git it has versioning, replication, audit history, and distributed backup — solved problems, inherited for free, using tools every developer already runs. Your content becomes portable in the most literal sense: a folder you can copy, grep, and diff.

Grav demonstrated how pleasant this model can be on the authoring side, and Dune deliberately keeps its conventions close — ordered folders like 01.home/, frontmatter in YAML, taxonomy in the page header, media co-located with the page it belongs to. Close enough, in fact, that Dune ships a migration command that imports a Grav site mostly as-is, along with importers for WordPress and Hugo. Hugo and the static site generators proved the performance argument long ago. But static generation gives up the server, and with it everything a server makes easy: authentication, forms, comments, search, personalization, an admin panel. Dune keeps the flat files and keeps the server. Pages render server-side on request, with caching and ETags doing the work that a static build would otherwise front-load.


Echoes of Helma

Readers of the previous article will remember Helma, the Javascript application server on the JVM whose community I was part of for many years. Building Dune, I kept noticing how many of Helma's ideas were simply good ideas, waiting for a runtime that could express them without the surrounding ceremony.

Helma had skins — HTML templates with placeholders that called macros, kept strictly separate from the logic that fed them. Dune's themes are the same separation with modern syntax: TSX templates that receive the fully rendered page and return the document. Templates are server-only; no Javascript from a template is ever sent to the browser. Where Helma's skins called macros, Dune's templates render components. And where E4X once let Helma assemble markup as a native language feature, Fresh's JSX — with Deno handling the compilation transparently — finally delivers that same no-visible-build-step experience that we lost when E4X was dropped from the language.

Helma mapped the request path onto an object tree; Dune does the same by mapping it onto the folder tree, giving you direct file system access to the object tree and the ability to directly colocate static data. A folder is a route, a file is a page, and the ordering prefix in the folder name is the navigation order. It is the kind of convention that explains itself the first time you see a content directory listing.

The deeper inheritance is architectural restraint. Helma was a framework you could hold in your head. The web stack since then accumulated layers — transpilers, bundlers, lock files for the lock files — that each solved a real problem and collectively buried the mental model. Deno's whole proposition is shedding those layers without losing what they provided, and Dune tries to be the same kind of tool one level up: a CMS you can hold in your head.


Zero Javascript by Default

Dune adopts Fresh's islands architecture wholesale, and it is worth spelling out what that means for a content site: by default, pages ship no Javascript at all. The server renders HTML, the browser displays it. A blog built with Dune sends markup and stylesheets and nothing else — the reader's browser does not parse a single script to display an article.

Interactivity is opt-in, per component. A search box, an image carousel, a comment form — each is an island, a small Preact component that hydrates independently while the rest of the page stays inert HTML. Themes declare islands by putting them in an islands/ folder; Dune discovers and bundles them at startup. The bundling happens once, when the server boots, not as a build step you run and commit. There is no dist/ directory in a Dune project.

This is the part of the modern stack I most wanted to preserve from the static-site world: the discipline of paying for Javascript only where it buys something. Thirty years of making Javascript fast, as celebrated at length in the previous article, should not be an excuse to ship megabytes of it to render paragraphs.


Growing into an Application

The trouble with most minimal systems is the cliff at the end of them: the day your project needs one feature the minimal system lacks, you rewrite on something heavier. Dune is designed to not have that cliff, which is why I describe it as a CMS that grows from Markdown content to full-stack web apps.

The growth path is incremental. Frontmatter queries become collections — a blog index is a folder plus a few lines of YAML declaring how to list its children. Taxonomies tag pages and generate the archive views. When structured data outgrows frontmatter, flex objects provide schema-defined records with generated admin CRUD. Forms, comments, and webhooks are built in. Public-site authentication supports passwords, OAuth, and magic links, with role-based access control and content gating when parts of a site go members-only. Multisite serves several sites from one process. And under all of it, a plugin system exposes the engine's hooks — response transforms, custom format handlers, admin services, scheduled jobs, browser entry points — so the features Dune doesn't have can be added without forking the ones it does.

Each of these is dormant until used. A Dune site that is just a blog carries none of the weight; the same installation can later take payments without changing platforms. That continuity — the same files, the same conventions, from brochure site to application — is the design goal that shaped most decisions.


Editing Where the Content Lives

There is an obvious gap in any flat-file system: the content is files, but not everyone who needs to edit it wants to open a text editor. Dune ships an admin panel — pages, media, users, history — but the part I find more interesting is inline editing, because it shows how the plugin architecture and the flat files cooperate.

Themes mark editable regions with typed marker components — EditableMarkdown for the body, EditableText for inline fields — imported from core and rendered server-side, shipping no JavaScript of their own. That marker contract is the entire coupling between a theme and the editing system. The editor itself is a plugin: it finds the markers, attaches a floating edit handle for logged-in admins, and opens a WYSIWYG editor over the page's Markdown source. Several admins can edit the same page at once; their changes merge conflict-free through a CRDT and land in the same Markdown file on disk that you could equally well have opened in a text editor. The file stays the single source of truth, whichever door you came in through.

And because markers are an admin-only contract, Dune scrubs them from every response served to anyone without a validated editing session. Anonymous visitors get clean markup with no editing fingerprint and no hints about the content's location on disk. Defaults like that one are scattered throughout: Deno's permission sandbox means the process touches only what it is explicitly granted, editing endpoints authenticate server-side regardless of what any HTML claims, and content history records who changed what. Security as the default posture, in the runtime and in the application — that lesson the ecosystem learned the hard way, and Dune inherits it deliberately.


Content an Agent Can Work With

The decision to keep content as files turns out to matter beyond the human authoring case. Markdown is what large language models are trained on — they write it fluently, read it without parsing errors, and never hallucinate syntax for it the way they occasionally do with proprietary export formats or CMS-specific template languages. A flat-file site's full content is visible to any tool that can read a directory. The folder-as-URL convention is legible enough that an agent can understand a site's complete structure from a directory listing, without consulting API documentation or a database schema. Frontmatter YAML is structured and predictable — an agent can read a page's metadata, patch a field, and write it back without a GUI, a REST call, or a migration script.

The absence of a build step matters too. An agent that creates a new content file can validate it immediately against a running server — there is no pipeline to understand, no warm-up phase, no output directory to inspect. And because content is git-tracked by convention, agents get the same safe working environment a developer gets: branch, apply, verify, commit or roll back. Dune is designed with AI agents in mind — and the flat-file model is why it works: plain files, a direct filesystem interface, no build step, git-tracked content, a running server you can query. The same choices that make it simple for a developer make it legible for an agent.

Tools for Agents

Dune ships skill files — concise domain references installed into .claude/skills/ by dune new and updatable via dune update:skills. Each skill covers one area of the system: content conventions, the plugin model, the schema layer, authentication, background jobs, email. An agent starting a new session reads the relevant skills and immediately has the pattern knowledge it needs without trawling documentation or inferring conventions from source code.

Beyond that, the built-in MCP server exposes nine read tools over stdio — querying pages, running searches, inspecting collections, reading taxonomy, examining config, listing available templates and blueprints, and fetching a page's raw source. An agent can get a complete picture of a running site without ever touching the filesystem.

For writes, the Change API (POST /admin/api/dev/apply) accepts structured JSON operations — create or update a file, delete a file, patch frontmatter fields, change config, install a plugin — with a dry-run mode that reports what would change before anything touches disk. dune validate runs the same checks the server runs on startup, so an agent can preflight a set of changes before committing them. The config schema is exportable as JSON Schema for IDE and agent autocompletion. And llms.txt and llms-full.txt are served from the docs site in the format the ecosystem has converged on for making documentation available to models.


Open from the Start

The previous article traced how Netscape, having gotten so much right technically with LiveWire, undid it all by keeping the implementation proprietary — trying to own the web instead of participating in it. WebCrossing repeated the mistake, and Helma, though open source, arrived before the distribution infrastructure existed that could have carried it to a wide audience.

Dune gets to take the opposite path on every count, with thirty years of accumulated infrastructure making it almost effortless. The code is MIT-licensed. It is published on JSR, the TypeScript-native registry, where every release carries its types, its documentation, and provenance attestation linking the package to the exact commit and CI run that produced it. Installing the CLI is one command; upgrading a site is editing a version number in one file. The themes are open, the plugin interfaces are documented, and the docs themselves are a public git repository — served, naturally, by a Dune site.

None of that guarantees adoption, and after thirty years I am under no illusion that the better-engineered system wins by default. But the failure modes of the closed path are documented history, recounted in my previous article. Participating in the open ecosystem is the only strategy that has ever compounded.

Dune is young, the version numbers still start with zero, and there is plenty left to build. If flat files, zero-Javascript-by-default pages, and a CMS you can hold in your head sound like your kind of stack, the place to start is getdune.org — the quickstart has you serving a site in five minutes, and the migration commands will be happy to carve your content out of whatever database it is currently sitting in.

The files are ready when you are.