--- title: how this blog works description: markdown in a git repo, a webhook, and no build step. here is the whole thing. date: 2026-09-08 tags: [meta, nuxt, gitea] draft: false --- This post is a file. It lives in a git repo, it has YAML frontmatter, and pushing it is the entire publishing pipeline. No CMS, no rebuild, no `npm run deploy`. ## the shape of a post Everything above the second `---` is frontmatter: ```yaml --- title: how this blog works # required-ish; falls back to the filename description: one line, used in the # optional; falls back to the first paragraph list view and the og tags date: 2026-09-08 # optional; falls back to a YYYY-MM-DD filename prefix updated: 2026-09-09 # optional tags: [meta, nuxt, gitea] # optional; also accepts "meta, nuxt" slug: how-this-blog-works # optional; falls back to the filename draft: false # true hides it in production --- ``` The filename becomes the slug, with a leading date stripped: `2026-09-08-hello.md` → `/blog/hello`. ## how it gets here 1. You push to the content repo. 2. Gitea fires a webhook at `/api/webhook/content` with an HMAC signature. 3. The server checks the signature, drops its cache, and re-reads the tree. 4. The next request renders from the fresh copy. If the webhook never fires — network hiccup, misconfigured secret — the cache still expires on its own after `CONTENT_TTL` seconds. And if the repo is down when a refresh is due, the last good copy keeps being served instead of a 502. ## code and images Fenced blocks are highlighted server-side by Shiki, in both themes at once: ```rust fn main() { let payload = std::fs::read_to_string("post.md").expect("a post"); println!("{} bytes, zero javascript", payload.len()); } ``` Relative image paths resolve against the post's own directory in the repo, so `![a diagram](./diagram.png)` works if you commit the png next to the markdown. > Blockquotes, tables, and lists all render. Anything markdown-it understands. That's it. Write a file, push, done.