How I built this site — the Brain → site pipeline
This site has no back office. No database. No external CMS.
The CMS is my Second Brain — an Obsidian vault in ~/Brain/.
The principle
Every article you read here started life as a Markdown note in my digital brain.
The pipeline:
~/Brain/projets/portfolio/drafts/ → content/<section>/ → git push → Vercel
Copy a file, commit, push. The post goes live in 60 seconds.
Why not a real CMS
Sanity, Contentful, the Notion API, Strapi — I evaluated all of them.
The real constraint: I already produce content from Brain anyway. My notes are already in Markdown, already structured, already cross-linked. An external CMS adds friction for zero gain.
A .md file on the filesystem is:
- Versionable with git
- Openable in any editor
- Readable offline
- Free
Resolving cross-links
The problem: inside Brain, links between notes get written naturally.
See [Rule F-01](./f01-regle-balance.md) for the full pattern.On the site, that link needs to become /insights/f01-regle-balance. Not ./f01-regle-balance.md.
I built a remark plugin (remarkResolveLinks) that:
- Builds a manifest of every post at build time:
slug → Post - Intercepts
linknodes with relative.mdpaths - Replaces the URL with the correct Next.js route
visit(tree, "link", (node: Link) => {
if (!/^\.\.?\//i.test(node.url)) return;
const filename = node.url.split("/").pop() ?? "";
const post = manifest.get(filename);
if (post) node.url = post.url;
});Result: I write cross-links in Brain exactly the way I'd write them in Obsidian. They just work on the site, with zero modification.
The slug
Brain files carry a date prefix: 20260429-1100-article-moneroo-sdk.md.
I don't want that date in the URL. /articles/20260429-1100-article-moneroo-sdk is unreadable.
toSlug() strips the prefix:
function toSlug(filename: string): string {
return filename
.replace(/\.(md|mdx)$/, "")
.replace(/^\d{8}-\d{4}-/, "");
}Final URL: /articles/article-moneroo-sdk. Clean.
The frontmatter
Every .md file starts with a YAML block.
---
title: "XOF n'a pas de centimes — le bug qui m'a coûté 3h"
date: 2026-04-29
topic: paiements
order: 1
tags: [moneroo, xof, devises, bug]
status: published
related:
- f01-regle-balance
---topic and order group insights into sections on the /insights page.
related gets resolved into full Post objects — rendered as a "See also" section beneath the article.
status: archived pulls a post from the listings without deleting the URL.
The sorting bug
Gray-matter parses date: 2026-04-29 into a Date object, not a string.
My first sort used .localeCompare(). It doesn't blow up right away — Date.localeCompare returns undefined, which passes silently in dev and quietly scrambles post order in production.
The correct version:
const da = a.frontmatter.date ? new Date(a.frontmatter.date).getTime() : 0;
const db = b.frontmatter.date ? new Date(b.frontmatter.date).getTime() : 0;
return db - da;Always convert to a timestamp before comparing YAML dates.
The design
Inspired by nav.al/archive. Black. White. Nothing else.
Full palette:
- Background:
#090909 - Text:
#f0f0f0 - Meta:
#888888 - Separators:
#333333
Typeface: Jost 300/400/500. Zero icons. Zero decorative images.
The design constraint: if you strip away everything that isn't text, is the information still readable? On this site, the answer is yes.
The full workflow
- Write the draft in
~/Brain/projets/portfolio/drafts/ - Add the frontmatter (title, date, status, tags, related)
- Copy into
content/<section>/ git add content/ && git commit -m "contenu: ..." && git push
That's it.
Vercel picks up the push, rebuilds the site via SSG, and deploys. Every page is statically generated from the Markdown files through generateStaticParams.
The real value here isn't the site. It's that the pipeline costs zero friction.
If publishing is hard, you don't publish. If publishing is easy, you publish more often.