Getting started with MILL


MILL, Markdown In, Living Layouts, is a Markdown → GRAIN-pages CMS: feed it a folder of .md files (frontmatter + prose) and it renders them as real GRAIN pages by mapping Markdown nodes to components. It sits a layer above the stack (batch → grain → MILL), depending on both and extending neither. The full mapping model is ARCHITECTURE.md; this page is the fastest path to a rendered page.

Install

MILL is published on the public npm registry as @tjakoen/mill, alongside @tjakoen/grain:

{
  "dependencies": {
    "@tjakoen/batch": "^0.1.0",
    "@tjakoen/grain": "^0.1.12",
    "@tjakoen/mill": "^0.2.0"
  }
}

That is the whole setup: no .npmrc, no auth token. (These packages lived on GitHub Packages until 2026-07-30, whose registry demands a token even for public packages; they are on npmjs now and install anonymously.)

Inside the grain monorepo itself, MILL is a sibling workspace package (workspace:*), no install step needed there.

A minimal collection

A collection is a folder of .md plus a MillCollection describing where it lives and what route prefix it answers:

import { createMillRoutes, dirSource } from "@tjakoen/mill/serve.ts";

const collections = [
  {
    prefix: "/docs",
    title: "Docs",
    description: "Guides, rendered from Markdown.",
    source: dirSource("./content/docs"),   // a folder of .md files
  },
];

const serveContent = createMillRoutes({ collections });

// inside your own fetch handler, before your other routes:
const hit = await serveContent(new URL(req.url).pathname);
if (hit) return hit;

createMillRoutes returns a transport-generic pathname handler, (pathname) => Promise<Response | null>. null means "not a MILL route", so the caller falls through to whatever serves everything else. With no compose passed, the default chrome ships as written (no component-tag expansion); pass compose (BATCH's renderPage) once you're wiring an app, so the chrome and any escape-hatch component tags in the Markdown compose at request time.

Verify it

bun run dev
# then visit /docs            the index, built from every .md's frontmatter
#            /docs/<slug>      one entry, rendered from that file's body
#            /docs/<slug>.md   the raw source, MILL's "honest source" twin route

Every filename minus .md, lowercased, becomes its slug (GETTING-STARTED.mdgetting-started).

Next steps

  • Read ARCHITECTURE.md for the mapping model (frontmatter → layout, block →

component), the content-source port, and the escape hatch.

portfolio's own wiring as the worked example.

  • See it live: this very page renders through MILL, and so do the layer docs it builds on,

GRAIN's getting-started and BATCH's getting-started.

  • The canonical plan (design, seams, what's built vs. deferred) is

mill/PLAN.md.