Getting started with MILL


MILL, Markdown In, Living Layouts, is a content rendering engine: feed it a folder of .md files (frontmatter + prose) and it renders them as GRAIN pages by mapping Markdown nodes to GRAIN classes. Its published package depends on GRAIN; BATCH is an optional host server and component composer. 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 and depends on GRAIN. Install both packages with Bun:

bun add @tjakoen/grain @tjakoen/mill

With npm, use npm install @tjakoen/grain @tjakoen/mill. They resolve from the public npm registry, so a new project needs no .npmrc or registry token. Add BATCH separately if your app uses it as the server and component composer.

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.md → getting-started).

Next steps

  • Read ARCHITECTURE.md for the mapping model (frontmatter → layout, block → component), the content-source port, and the escape hatch.
  • Read ADD-A-COLLECTION.md for the full MillCollection shape, using the 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.