Document how to add and release a blog post

docs/blog-workflow.md: frontmatter/voice conventions, the draft+pubDate gate,
the daily deploy cron, publish checklists (scheduled vs immediate), and gotchas.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-01 21:44:12 +02:00
parent 6776b1f337
commit 0c686a2e40

117
docs/blog-workflow.md Normal file
View File

@@ -0,0 +1,117 @@
# Writing and releasing a blog post
How posts live, how they're scheduled, and how they go live. The short version:
**a post is public when it is `draft: false` *and* its `pubDate` has arrived**
and a daily job rebuilds the site so "arrived" keeps advancing on its own.
## 1. Add the post
Create a Markdown file in `src/content/blog/`, e.g.
`src/content/blog/my-post.md`. The filename (minus `.md`) is the URL slug:
`/blog/my-post/`.
Frontmatter is validated at build time against `src/content.config.ts`:
```markdown
---
title: Short, specific title
description: >-
One or two sentences. Shown in listings, the RSS feed, and social previews.
pubDate: 2026-07-08 # ISO date. The publish gate (see below).
# updatedDate: 2026-07-10 # optional; shown as "updated"
tags: [android, calendula] # see the tag vocabulary below
draft: true # true = never public; flip to false when ready
---
Body in Markdown. ~80-char wrapped to match the other posts.
```
Voice conventions (match the existing posts): first-person, "constraint as
design", link to standards/APIs inline, one punchy closing line. Reference other
posts with root-relative links like `/blog/open-standards`.
Established **tag vocabulary** — reuse these rather than inventing one-offs:
`android`, `architecture`, `calendula`, `caldav`, `open-standards`,
`open-source`, `accessibility`, `localization`, `meta`.
## 2. The three states
The gate lives in `src/utils/posts.ts` (`getPublishedPosts()`), which every
listing, the tag pages, the post routes, and `rss.xml` use.
| `draft` | `pubDate` | Dev server | Production |
| --- | --- | --- | --- |
| `true` | any | visible (preview) | **hidden** |
| `false` | future | visible (preview) | **hidden until the date** |
| `false` | today / past | visible | **live** |
So:
- **`draft: true`** — work in progress or not yet approved. Never public.
- **`draft: false` + future `pubDate`** — approved and *scheduled*. Publishes
itself on that day (see §3).
- **`draft: false` + past `pubDate`** — live now.
Preview anything (including drafts and scheduled posts) locally with
`npm run dev` — the dev server lifts the gate entirely.
## 3. How release actually happens
The site is static, so "now" is frozen at each build. Two things trigger a
rebuild:
1. **Push to `main`** → Coolify redeploys (the normal deploy).
2. **The daily cron**`.gitea/workflows/scheduled-deploy.yml` runs at
**06:15 Europe/Berlin**, pings the Coolify deploy hook, and the rebuild
re-evaluates the date gate. This is what makes a future-dated post appear on
its day without anyone touching the repo.
The cron runs on the shared **`docker`** runner and needs two repo secrets set
once (Gitea → Settings → Actions → Secrets): `COOLIFY_DEPLOY_HOOK` (the app's
deploy URL) and `COOLIFY_TOKEN` (a Coolify API token, Bearer).
## 4. Publish checklist
To schedule a post for its date (the normal path):
1. Set `draft: false`, leave the intended future `pubDate`.
2. Commit and get it onto `main` (branch → PR → merge; PRs via `tea pr create`
/ `tea pr merge`). Merging deploys, but the post stays hidden until its date.
3. Done — the daily cron publishes it on `pubDate`.
To release a post **immediately**:
1. Set `draft: false` **and** `pubDate` to today (or a past date), so the gate
passes now.
2. Merge to `main`.
3. Trigger a rebuild right away instead of waiting for the cron:
```sh
tea actions workflows dispatch scheduled-deploy.yml --ref main
```
(`tea` may print `unexpected end of JSON input` — that's it mis-reading
Gitea's empty success response; the run still starts. Check it with
`tea actions runs list`.)
4. Coolify rebuilds; confirm at `https://jeanlucmakiola.de/blog/<slug>/`.
## 5. Handy commands
```sh
npm run dev # preview everything locally
npm run build # production build (applies the gate)
tea actions workflows list # see the deploy workflow
tea actions workflows dispatch scheduled-deploy.yml --ref main # manual deploy
tea actions runs list # watch run status/conclusion
```
## 6. Gotchas
- **Don't** put non-post `.md` files under `src/content/blog/` — the collection
glob will try to parse them as posts and fail the build. Docs like this one
live in `docs/`.
- `pubDate` with no time resolves to `00:00 UTC` that day, so a post becomes
eligible at UTC midnight; the morning cron then publishes it. Adjust the cron
time in the workflow if you want a different local hour.
- Timezone for the cron is pinned in the workflow (`TZ=Europe/Berlin`); the
gate comparison itself uses the build machine's clock (UTC in CI).