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/millWith 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 routeEvery filename minus .md, lowercased, becomes its slug (GETTING-STARTED.md → getting-started).
Next steps
- Read
ARCHITECTURE.mdfor the mapping model (frontmatter → layout, block → component), the content-source port, and the escape hatch. - Read
ADD-A-COLLECTION.mdfor the fullMillCollectionshape, 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.