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.tsas 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,/notesroutes driven bycontent/demo/.- MDX support for long-form writing at
/notesand anywhere else undersrc/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 setrepofor a live star count.hrefauto-resolves tohttps://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 pointbrand.avatarat it insite.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**.Inline code — filenames, config keys, command names:
Edit `site.config.ts` to change everything.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).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 presets —
src/lib/theme/presets.ts,src/lib/theme/build-theme-style.ts. - Block registry —
src/lib/blocks/registry.ts,src/lib/blocks/register-defaults.ts. - Inline markdown — the hero body supports bold,
code, and[links](url)viasrc/lib/markdown/inline.tsx. Same helper is available for any block that renders short author copy. - MDX renderer —
mdx-components.tsxwires every.mdxpage intoButtercutProse; override it there to add global components (callouts, charts, embeds).