PolicykitDocs

Getting started

Install policykit, write your first policy revision on disk, and render it in a Next.js App Router page - in about five minutes.

3 min readUpdated 11 August 2026
Getting started

By the end of this page you have a terms-of-service revision stored on disk and rendered at /en/legal/terms in your Next.js app.

Prerequisites: a Next.js App Router app with React 18 or newer. The package's peer range starts at Next 14; the route sample below uses the async params shape, which is Next 15 and up.

Install the package

npm install @daanvandenbergh/policykit next-mdx-remote

next, react, and react-dom are peer dependencies you already have. next-mdx-remote (v5 or v6) is the fourth peer, and it is only needed for the @daanvandenbergh/policykit/react subpath - the part that renders MDX. The root entry imports nothing but node:fs, node:path, and gray-matter.

Create your first revision

A revision IS a directory named after the day you decided it. Create one file inside it, named after the locale:

policies/terms-of-service/
  2026-08-11/
    en.mdx

Write policies/terms-of-service/2026-08-11/en.mdx. The default locale's file carries the revision's metadata: effectiveFrom, notice, and changeSummary are required, title is optional.

---
effectiveFrom: 2026-08-11
notice: none
changeSummary: "First published terms of service."
title: "Terms of Service"
---

## 1. Agreement

By using the service you agree to these terms.
  • effectiveFrom - a real YYYY-MM-DD calendar day, on or after the revision date. Here they match, so the text binds immediately.
  • notice - none, notify, or reconsent. A first publish owes nobody notice.
  • changeSummary - a non-empty sentence. Internal evidence, never rendered to users.
  • title - optional.

The body must not be empty: the MDX body is the legal text, so frontmatter alone is an error.

Configure the policy

One instance per policy, in one module. dir must be absolute - the constructor rejects a relative path, because the same instance runs from several working directories and would otherwise point at different content per cwd.

// src/content/policies.ts
import path from "node:path";
import { Policy } from "@daanvandenbergh/policykit";

export const termsPolicy = new Policy({
    slug: "terms-of-service",
    dir: path.join(process.cwd(), "policies", "terms-of-service"),
    locales: ["en"],
});

locales is required - it is the set of languages you serve, and every locale file on disk must belong to it. defaultLocale is optional: it defaults to "en" and must be one of locales, so a single-locale English policy never sets it.

Render it

PolicyDocument is an async server component that renders one revision's MDX body. Omit revision and it picks the live-page default itself: the revision that binds today, or the newest published one if none binds yet.

// app/[locale]/legal/terms/page.tsx
import { PolicyDocument } from "@daanvandenbergh/policykit/react";
import { termsPolicy } from "@/content/policies";

export default async function TermsPage({ params }: { params: Promise<{ locale: string }> }) {
    const { locale } = await params;
    return (
        <article>
            <PolicyDocument policy={termsPolicy} locale={locale} />
        </article>
    );
}

Run npm run dev and open /en/legal/terms. You should see the heading "1. Agreement" and the sentence below it. That is your MDX body, compiled and served.

Nothing is styled and no title is printed - PolicyDocument renders the body only, and the page shell stays yours.

This route serves one locale, so it renders as written. Once the [locale] segment can hold a language the policy does not have, guard before rendering: PolicyDocument throws when the resolved revision has no file for that locale. Rendering policies has the guard-then-404 recipe.

When you publish your second version, writing a revision walks the authoring steps - or install /policykit, the Claude Code skill that runs those steps for you. For titles, archive routes, and the MDX component map, see rendering policies.

Was this page helpful?