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>
118 lines
4.7 KiB
Markdown
118 lines
4.7 KiB
Markdown
# 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).
|