---
title: "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`](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:

```sh
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:

```ts
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

```sh
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`](ARCHITECTURE.md) for the mapping model (frontmatter → layout, block →
  component), the content-source port, and the escape hatch.
- Read [`ADD-A-COLLECTION.md`](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](grain/docs/GETTING-STARTED.md) and
  [BATCH's getting-started](batch/docs/GETTING-STARTED.md).
- The canonical plan (design, seams, what's built vs. deferred) is
  [`mill/PLAN.md`](https://github.com/tjakoen/grain/blob/main/packages/mill/PLAN.md).
