Tutorial

Guide

Turn Buttercut into your personal site in about thirty minutes. Every step has a stable anchor so READMEs, commit messages, and JSDoc can link straight to the relevant walkthrough.

This page itself is an .mdx file living at src/app/guide/page.mdx — a working example of everything the theme supports.

What you are getting

  • A typed site.config.ts as the single source of truth.
  • A home page assembled from small, swappable blocks (hero, status row, projects, integrations panel) listed in home.blocks.
  • /about, /projects, /notes routes driven by content/demo/.
  • MDX support for long-form writing at /notes and anywhere else under src/app/.
  • Integrations (GitHub stars, Last.fm, Weather) that stay silent unless you turn them on.

Map of the repo

buttercut/
├── site.config.ts              ← start here
├── content/demo/               ← your copy lives here
│   ├── intro.md                ← hero body
│   ├── about.json              ← /about structured sections
│   ├── projects.json           ← /projects cards
│   └── notes/*.mdx            ← each one is a note
├── src/app/                    ← App Router routes
├── src/blocks/                 ← home-page sections (Buttercut*)
├── src/components/             ← shared UI
├── src/custom/                 ← your overrides (survives updates)
└── src/lib/
    ├── config/                 ← types + defaults
    ├── theme/                  ← tokens + colour presets
    └── integrations/           ← github / lastfm / weather

Step 1

Clone and run#

git clone https://github.com/kaiiiichen/buttercut.git my-site
cd my-site
npm install
npm run dev

Open http://localhost:3000. You should see the Buttercut demo — it already reads your live GitHub stars because integrations.github is on by default.

Step 2

Fill in site.config.ts#

import { createSiteConfig } from "@/lib/config/create-site-config";

export const siteConfig = createSiteConfig({
  site: {
    title: "Your Name",
    description: "Personal site, notes, projects.",
    siteUrl: "https://yourdomain.com",
  },
  nav: [
    { label: "About", href: "/about" },
    { label: "Projects", href: "/projects" },
    { label: "Notes", href: "/notes" },
    { label: "Guide", href: "/guide" },
  ],
  socials: [
    { id: "github", label: "GitHub", href: "https://github.com/yourhandle" },
    { id: "x", label: "X", href: "https://x.com/yourhandle" },
    { id: "email", label: "Email", href: "mailto:you@example.com" },
  ],
});

Only override what differs from defaults — everything else is filled in from src/lib/config/defaults.ts. Social icons in the hero are picked by id (github, linkedin, x, email, docs); an unknown id falls back to showing the label as text.

Step 3

Swap the content in content/demo/#

  • Hero body — edit content/demo/intro.md. Supports inline bold, code, and [links](url).

  • About page — edit content/demo/about.json. Structured sections (intro, education[], experience[], volunteering[], focus[]) render into the editorial two-column layout. Every section is optional; empty arrays hide that card entirely. All string fields support the inline bold / code / [link](url) subset.

  • Projects — edit content/demo/projects.json. Every entry can set repo for a live star count. href auto-resolves to https://github.com/<repo> when omitted:

    {
      "name": "buttercut",
      "description": "This theme.",
      "repo": "kaiiiichen/buttercut",
      "tags": ["Next.js", "TypeScript"]
    }
    
  • Avatar — drop a square image in public/ and point brand.avatar at it in site.config.ts (defaults to /avatar-placeholder.svg).

Step 4

Authoring short copy#

Every short-copy surface in the theme — the hero intro, project description and tags[], and note summary frontmatter — runs through a single inline markdown helper at src/lib/markdown/inline.tsx. It recognises exactly three tokens, emits real React nodes, and never touches dangerouslySetInnerHTML. See the Inline markdown subset section of the README for the canonical reference.

Bold — draw the reader's eye to a single word:

Buttercut is **theme-first**.
Buttercut is theme-first.

Inline code — filenames, config keys, command names:

Edit `site.config.ts` to change everything.
Edit site.config.ts to change everything.

Links — http(s), mailto, and relative paths render as real anchors; any other scheme falls back to raw text (so a stray [x](javascript:…) in author copy is inert):

More in the [guide](/guide).
More in the guide.

Need tel: or sms:? Extend the allow list once in site.config.ts:

export const siteConfig = createSiteConfig({
  content: {
    allowedLinkSchemes: ["http", "https", "mailto", "tel", "sms"],
  },
});

A built-in hard-deny list — javascript, data, vbscript, file — always wins, so opting in by accident still cannot produce an exploitable anchor.

Step 5

Pick a colour mood#

import { buttercutPreset } from "@/lib/theme/presets";

brand: {
  theme: { ...buttercutPreset("sunset"), accent: "#ff3366" },
}

Three presets ship out of the box: sunset, ocean, terminal. Spread one and override any token, or define the whole token set inline:

brand: {
  theme: {
    accent: "#0b6ea4",
    accentDark: "#7dd3fc",
    background: "#f2f7fb",
    backgroundDark: "#0b1e2b",
    foreground: "#0b2a3c",
    foregroundDark: "#e2f1fb",
  },
}

Values are sanitised before being written into a <style> tag — anything outside the safe CSS-colour charset is silently dropped, so the page never crashes on a typo.

Step 6

Reorder or hide home blocks#

The home page renders home.blocks in order, top to bottom, showing only the entries whose enabled is true:

home: {
  blocks: [
    { id: "hero", enabled: true },
    { id: "status", enabled: true },       // Listening + Location side-by-side
    { id: "demo_projects", enabled: true },
    { id: "integrations", enabled: false }, // hide the debug panel
  ],
}

Built-in ids: hero, status, now_playing, weather, demo_projects, integrations. Drop status and list now_playing and weather separately if you want the two cards stacked instead of side-by-side.

Step 7

Add or override a block#

Put your own components under src/custom/ — that directory is reserved for user code and survives theme updates. A runnable example lives at src/custom/blocks/MyHero.tsx; flip the switch in src/custom/register.ts:

import { registerButtercutBlock } from "@/lib/blocks/registry";
import { MyHero } from "./blocks/MyHero";

export function applyButtercutCustom(): void {
  registerButtercutBlock("hero", MyHero);           // override default
  registerButtercutBlock("changelog", Changelog);   // brand-new id
}

Any new id can then appear in home.blocks. The built-in ButtercutHero also accepts a slots prop (avatar, title, tagline, body, socials) if you just want to swap one piece without forking the whole block.

Step 8

Write notes in MDX#

Every note under content/demo/notes/ is an .mdx file. Write plain markdown most of the time; drop to JSX when you need a component inline (callouts, charts, live demos).

Add the file, then register it with a single line in src/lib/demo/mdx-notes.ts:

export const BUTTERCUT_MDX_NOTES = {
  "my-essay": () =>
    import("../../../content/demo/notes/my-essay.mdx"),
};

The registry is the authoritative list — generateStaticParams reads it directly, so anything unregistered returns 404 and no accidentally-published draft can leak to prod.

Step 9

Turn on optional integrations#

Each integration is off unless you configure it. Add flags in site.config.ts, then environment variables from .env.example:

integrations: {
  github:  { enabled: true },                  // on by default
  lastfm:  { enabled: true, username: "kai" }, // needs LASTFM_API_KEY
  weather: { enabled: true, lat: 37.87, lon: -122.26, label: "Berkeley" },
}

Every fetch has a null fallback, so a rate limit or outage never breaks a page — you get a placeholder card instead.

Step 10

Deploy#

Buttercut is a regular Next.js 16 app:

npm run build

On Vercel, import the repo and set any integration env vars in the project settings. Everything on /, /about, /projects, /notes, /notes/[slug], and /guide pre-renders statically with a one-hour revalidate for live star counts.

Where to dig deeper

  • Theme tokens and presetssrc/lib/theme/presets.ts, src/lib/theme/build-theme-style.ts.
  • Block registrysrc/lib/blocks/registry.ts, src/lib/blocks/register-defaults.ts.
  • Inline markdown — the hero body supports bold, code, and [links](url) via src/lib/markdown/inline.tsx. Same helper is available for any block that renders short author copy.
  • MDX renderermdx-components.tsx wires every .mdx page into ButtercutProse; override it there to add global components (callouts, charts, embeds).