I had a decade of drafts, half-finished essays, and “this will make a good post someday” pages rotting in Notion. Every time I opened the app the sidebar got taller and the tree got heavier and the thing I wanted to write got further away.
This weekend I tore it down.
Why I left Notion
Notion is a database pretending to be a notebook. That’s fine when you need a database. When you want to publish, it fights you. Pages live in a tree nobody else can see. Links break when you move things. Export is an afterthought — dump a zip of markdown that half-renders because the image paths are UUIDs and the front matter is gone.
Years of trying to turn a Notion workspace into a public blog taught me the same lesson twice: the content isn’t in the app, it’s trapped in the app. Anything not in plain markdown under git is future-me’s problem.
So the goal was boring. Plain markdown. Git history. Static HTML. Something I can deploy with one command and still read in ten years.
Picking Astro + AstroPaper
Not Next — I don’t need a React app to render paragraphs.
Not Hugo — I’ve used it, it’s fast, and every time I want to change something I spend twenty minutes fighting Go templates.
Astro hit the shape I wanted. Content collections with a typed frontmatter schema. MDX if I need it, markdown if I don’t. Zero JS on the page unless I ask for it. Shiki code highlighting baked in. The whole site compiles to a folder of static files I can host anywhere.
AstroPaper gave me a starting point so I wasn’t designing from scratch at midnight. I took the skeleton and stripped everything else.
The migration weekend
Two source repos: CatfixSite (47 posts, some good, some abandoned) and astro-paper (5 newer posts and a folder of drafts I’d forgotten about). First pass was mechanical — copy markdown files into src/data/blog/, fix frontmatter, validate against the content collection schema.
The schema catches everything. Quoted date strings. Missing authors. Descriptions that were actually URLs I’d pasted in and never cleaned up. Over thirty posts had descriptions longer than 160 characters, or worse — the full first paragraph of the post shoved into the description field. I cleaned them.
Drafts came over with draft: true so they don’t render but they’re there, in the tree, waiting. Twenty-four of them. That’s the pile I’d been telling myself I’d “finish this week” for three years.
Two things got left behind on purpose. The Bobby and Katana pages — project landing pages from a previous life, not worth migrating. And the old privacy page, which was template boilerplate from a site I don’t run anymore. If I need one, I’ll write one.
Images were a disaster. Most of them came through blurry — old screenshots from phone cameras, scaled up by whatever CMS had chewed on them last. I tried to fix a few. Then I looked at the backlog of ninety images and made the call.
.prose img {
display: none;
}
Gone. Every image in every post. If a post needed the image to make sense, I’d have to rewrite the post. Turns out most of them didn’t.
Design by subtraction
This is the part I care about. Every decision was “what can I delete.”
The theme toggle? Ripped it. I forced dark mode and deleted both copies of theme.ts.
The <Archives> section with its separate route and its pagination and its “all posts ever”? Gone. Posts is posts. Twenty per page. showArchives: false.
The border-l-4 border-accent strip down the left side of every resume entry? “Looks like sht.” Deleted.
Dashed underlines on hover. Wavy squiggly nav underlines. “Fix the hover of nav link it is a siqqly line.” Replaced with a solid underline and this, globally:
a:hover::after {
content: " <GO>";
font-size: 0.75em;
opacity: 0.7;
}
a:has(h1, h2, h3, h4, h5, h6, img, svg):hover::after {
content: none;
}
Every text link gets a little <GO> on hover. Links wrapping headings or images don’t, because it looked stupid there. That’s the whole interaction design.
The § symbols before bio headings. The “01 — About” section label on the index. Decorative chrome that added nothing. Out.
The About page got merged into the index under the hero. One fewer nav link. One fewer route. Header nav is now Posts, Tags, Resume, Search. Four links. If I need more, I’ll add more.
The stack that survived
Seventeen thousand two hundred and fifty-five lines of code, counting config and content. Most of that is the 75 markdown posts. The actual site code is small.
// astro.config.ts — the useful bits
shikiConfig: {
themes: { light: "catppuccin-latte", dark: "catppuccin-mocha" },
transformers: [
transformerFileName({ style: "v2" }),
transformerNotationHighlight(),
transformerNotationWordHighlight(),
transformerNotationDiff({ matchAlgorithm: "v3" }),
],
},
experimental: {
fonts: [{
name: "Google Sans Code",
cssVariable: "--font-google-sans-code",
provider: fontProviders.google(),
weights: [300, 400, 500, 600, 700],
}],
},
Catppuccin Mocha for dark, Latte for light. JetBrains Mono via Astro’s new experimental font pipeline so I’m not pulling a Google Fonts <link> tag at runtime. Terminal-window dots (red, yellow, green) rendered with ::before on every code block — that’s the only place I let the terminal aesthetic leak through. Not the nav, not the headings, not the resume. Just the code.
The color tokens are five lines:
:root {
--background: #1e1e2e;
--foreground: #cdd6f4;
--accent: #ffffff;
--muted: #a6adc8;
--border: #313244;
}
Accent is white, not mauve, not blue. I tried accent colors. They all felt like theme decisions that were louder than the writing. White gets out of the way.
Pagefind for search. Dynamic OG images via Satori + Sharp. Cloudflare Pages for hosting — git push and three minutes later it’s live.
Working with Claude Code
Honest take: Claude Code is very good at the boring parts and needs steering for the opinionated parts.
Good at: reading 75 markdown files and fixing frontmatter across all of them. Writing content-collection schemas from an example. Porting a resume from one set of HTML idioms to another. Rewriting a component when I paste in the broken version and say “this looks terrible.”
Needs steering: every single visual decision. The first pass at the resume had border accents I hated. The first nav hover was wavy. The first hover underline was dashed. Every time I said “no, cleaner” it got cleaner. Every time I said “looks like sht” it tried something new. I wasn’t paying for code, I was paying for a back-and-forth with something that would actually implement the next attempt in thirty seconds instead of thirty minutes.
One real moment: mid-migration I looked at my terminal and asked “did you write ANY beads issues?” — because I’d told it to track work in bd and it had just been coding. That’s the pattern. It’ll do the task. It won’t do the process around the task unless you hold it to it. So hold it to it.
The commits tell the arc better than I can:
8ecb898 feat: restore blog with 50 posts migrated from CatfixSite
9338356 feat: migrate drafts, apply Catppuccin theme, clean up UI
4104dcc refactor: strip dead theme toggle and index/resume clutter
c6d40e2 refactor: remove bloat, fix bugs, simplify components
a369491 fix: accent to white, headshot favicon, clean up styles
166c2bb refactor: remove dead config, components, and cruft
Three of the last six are refactor: remove. That’s the project.
What’s next
Twenty-four drafts with draft: true in the frontmatter. Some of them are worth finishing. Most of them are worth reading once and deleting. I’ll do that in public, in the open repo, with real commit messages, instead of in a Notion sidebar that only I can see.
Notion is still open in a tab somewhere. It can stay there.