diff --git a/docs/blog-workflow.md b/docs/blog-workflow.md new file mode 100644 index 0000000..752b0d8 --- /dev/null +++ b/docs/blog-workflow.md @@ -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//`. + +## 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).