Compare commits
2 Commits
release-wi
...
docs-blog-
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c686a2e40 | |||
| 6776b1f337 |
117
docs/blog-workflow.md
Normal file
117
docs/blog-workflow.md
Normal 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).
|
||||
Reference in New Issue
Block a user