Files
jeanlucmakiola.de/docs/blog-workflow.md
Jean-Luc Makiola 0c686a2e40 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>
2026-07-01 21:44:12 +02:00

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 + 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:

    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

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).