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>
4.7 KiB
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:
---
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+ futurepubDate— approved and scheduled. Publishes itself on that day (see §3).draft: false+ pastpubDate— 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:
- Push to
main→ Coolify redeploys (the normal deploy). - The daily cron →
.gitea/workflows/scheduled-deploy.ymlruns 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):
- Set
draft: false, leave the intended futurepubDate. - Commit and get it onto
main(branch → PR → merge; PRs viatea pr create/tea pr merge). Merging deploys, but the post stays hidden until its date. - Done — the daily cron publishes it on
pubDate.
To release a post immediately:
-
Set
draft: falseandpubDateto today (or a past date), so the gate passes now. -
Merge to
main. -
Trigger a rebuild right away instead of waiting for the cron:
tea actions workflows dispatch scheduled-deploy.yml --ref main(
teamay printunexpected end of JSON input— that's it mis-reading Gitea's empty success response; the run still starts. Check it withtea actions runs list.) -
Coolify rebuilds; confirm at
https://jeanlucmakiola.de/blog/<slug>/.
5. Handy commands
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
.mdfiles undersrc/content/blog/— the collection glob will try to parse them as posts and fail the build. Docs like this one live indocs/. pubDatewith no time resolves to00:00 UTCthat 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).