Publishing Blog Posts
Blog source-of-truth model, frontmatter spec, and publish flow
Publishing Blog Posts
`docs/blog/` in the `aiwg` repo is the single source of truth for AIWG blog content. You write a post once as markdown here; pagenary builds it to docs.aiwg.io and auto-emits a machine-readable manifest, and aiwg.io sources the list from there. One place to write, one merge to publish — no hand-authored HTML on the website.
This page is the contract the downstream consumer (aiwg.io, `aiwg.io#60`) builds against.
The model
docs/blog/<slug>.md (frontmatter + body) ← authoritative content (this repo)
docs/.public/blog/<image>.png ← post images (served as static assets)
docs/config.json "collections" ← marks docs/blog as a collection
│ pagenary build (.gitea/workflows/docsite-build.yml → docsite-deploy.yml)
▼
docs.aiwg.io/pages/blog--<slug>.html ← rendered post (crawlable static page)
docs.aiwg.io/blog/index.json ← AUTO-GENERATED manifest (from frontmatter)
docs.aiwg.io/blog/feed.xml ← AUTO-GENERATED RSS feed
│ fetched at build by aiwg.io's vite-plugin-blog (aiwg.io#60)
▼
aiwg.io/blog ← public list, links to / mirrors docs.aiwg.io
The rule: content is single-sourced in this repo. The manifest is generated by pagenary from each post's frontmatter (no hand-maintained index). The website reads the manifest and renders or links out.
Requires `@pagenary/publisher` ≥ 2026.5.3 (collection support, pagenary#18).
Where things live
| Item | Location | Notes |
|---|---|---|
| Post markdown | `docs/blog/<slug>.md` | `<slug>` = filename stem = URL slug |
| Post images | `docs/.public/blog/<file>.png` | served at `https://docs.aiwg.io/assets/blog/<file>.png` |
| Collection config | `docs/config.json` → `collections[]` | marks `docs/blog` as a collection (one-time, already set) |
| Section nav | `docs/blog/_manifest.json` + the top-level `docs/_manifest.json` `order` | makes the post reachable in the docs nav |
| Manifest + feed | generated → `docs.aiwg.io/blog/index.json`, `/blog/feed.xml` | do not hand-author these |
Use the date-prefixed kebab slug, e.g. `2026-5-how-aiwg-builds-your-system-prompt`.
Collection config (already wired)
`docs/config.json` carries the one-time collection registration:
"collections": [
{
"path": "blog",
"route": "/blog",
"title": "AIWG Blog",
"manifest": true,
"feed": true,
"sortBy": "date",
"order": "desc"
}
]
`path` is relative to the docs content root (`docs/`). On every build, pagenary reads every `.md` in `docs/blog/` (excluding `_`-prefixed files and `index.md`) and emits `dist/.../blog/index.json` + `feed.xml`. You don't touch this per-post.
Frontmatter spec
Every post opens with YAML frontmatter. pagenary's collection generator derives the manifest from it. Note which fields it reads verbatim vs derives/overrides — this is exact behavior verified against `@pagenary/publisher` 2026.5.3:
---
title: "How AIWG builds your customized system prompt (and command set)"
slug: "2026-5-how-aiwg-builds-your-system-prompt"
date: "2026-05-26"
summary: "One or two sentences. Lead with the answer. Shown in the blog list, social cards, and RSS."
hero: "https://docs.aiwg.io/assets/blog/system-prompt.png"
reading_time: 9
status: "published"
canonical: "https://aiwg.io/blog/2026-5-how-aiwg-builds-your-system-prompt"
tags: ["ai-coding", "developer-tools", "open-source"]
# --- optional (provenance / categorization, ignored by the manifest) ---
pillar: "2 how/why"
audience: "developers customizing AI coding assistants across more than one tool"
aiwg_refs: ["aiwg use", "aiwg regenerate", "path-scoped rules"]
---
| Field | Required | How pagenary uses it in the manifest |
|---|---|---|
| `title` | yes | Verbatim (falls back to first `# Heading`, then slug). Also repeat as a leading `# Title` in the body. |
| `slug` | yes | Convention only — pagenary uses the filename stem as the slug. Keep them equal. |
| `date` | yes | Verbatim. Drives manifest sort order (`sortBy: date`, newest first). |
| `summary` | yes | Verbatim (falls back to `description`). |
| `hero` | yes | Verbatim, not absolutized — so use a full `https://docs.aiwg.io/assets/blog/<file>.png` URL (the consumer fetches the manifest cross-origin and needs an absolute image URL). |
| `tags` | recommended | Verbatim (array). pagenary reads `tags` — not `hashtags`. |
| `reading_time` | informational | Ignored by the manifest — pagenary computes it from body word count (~200 wpm). Keep it accurate for humans, but the manifest value is computed. |
| `canonical` | informational | Ignored by the manifest — pagenary derives the manifest `canonical` to the crawlable docs.aiwg.io static page (`/pages/blog--<slug>.html`). Keep it to record author intent. |
| `status` | convention only | Not a filter — see Drafts. Every post file in `docs/blog/` is published. |
| `pillar`, `audience`, `aiwg_refs` | no | Provenance only; not in the manifest. |
Do not add an `authoring:` field or any AI-tool credit. The repo-wide `no-attribution` rule applies to blog posts.
Always repeat the title as a leading `# Title` in the body — pagenary strips the YAML frontmatter and renders only the body, so without a leading heading the page has no `<h1>`. (`@pagenary/publisher` ≥ 2026.5.4 wires the frontmatter parser into the page renderer too, closing pagenary#19.)
Images
Place post images under `docs/.public/blog/` and reference them by their served absolute path:
- Body embed: `<img src="/assets/blog/<file>.png" alt="alt">`
- Frontmatter `hero`: `https://docs.aiwg.io/assets/blog/<file>.png`
`docs/.public/` is pagenary's static-asset directory; its contents are copied under `/assets/` in the built site (`docs/.public/blog/x.png` → `docs.aiwg.io/assets/blog/x.png`). Co-located `docs/blog/images/` files are NOT copied by the build — use `.public/blog/`. Keep heroes 16:9 with descriptive alt text.
Tools & transparency disclosure (required)
Every post must end with a `## Tools & transparency` section that honestly discloses how the content was produced. AIWG publishes how its own content is made; readers should never have to guess. Be truthful, not flattering — most AIWG posts are mostly AI-generated and then human-fact-checked, and the disclosure should say exactly that rather than overstate human authorship.
Use this consistent shape (adapt the imagery + facts lines per post):
## Tools & transparency
AIWG is open about how its content is made.
- **Words:** mostly AI-generated, then fact-checked and edited by a human.
- **Facts and claims:** checked against the actual work/release — nothing model-invented ships unverified.
- **Imagery:** which images, if any, were AI-generated and with what tool (e.g. ChatGPT / DALL·E); confirm no text/logos are AI-rendered. ("none" if there are no images.)
- **Final pass:** human.
Net: AI-drafted, human-fact-checked.
Guidance:
- Don't over-claim originality. If the prose was drafted by a model (the common case), say "mostly AI-generated." Reserve "original human writing" for posts a person actually wrote start to finish.
- The human contribution is fact-checking and editing — that's the honest, valuable part, and it's what the disclosure should foreground.
- This is a disclosure of content provenance, distinct from the `no-attribution` rule (which forbids stamping AI-tool credit on commits/code). Marketing prose and imagery get an honest provenance note; commits do not get tool attribution.
Manifest (generated — do not hand-edit)
pagenary writes `docs.aiwg.io/blog/index.json` on every build as an envelope object:
{
"title": "AIWG Blog",
"route": "/blog",
"count": 1,
"generated": "2026-05-27T17:03:29.776Z",
"posts": [
{
"slug": "2026-5-how-aiwg-builds-your-system-prompt",
"title": "How AIWG builds your customized system prompt (and command set)",
"date": "2026-05-26",
"summary": "AIWG doesn't ship one static system prompt. …",
"hero": "https://docs.aiwg.io/assets/blog/system-prompt.png",
"tags": ["ai-coding", "developer-tools", "open-source"],
"reading_time": 9,
"canonical": "https://docs.aiwg.io/pages/blog--2026-5-how-aiwg-builds-your-system-prompt.html",
"path": "/blog/2026-5-how-aiwg-builds-your-system-prompt"
}
]
}
Consumer contract notes for aiwg.io#60:
- It is an object (`{title, route, count, generated, posts[]}`), not a bare array. Iterate `posts`.
- `canonical` points at the docs.aiwg.io rendered page (pagenary makes docs.aiwg.io canonical); `path` is the clean `/blog/<slug>` route.
- `hero` is whatever the frontmatter declared — keep it an absolute URL so it renders without rewriting.
- `feed.xml` (RSS 2.0) is emitted alongside for feed readers / sitemap.
- Sorted by `date` descending.
Drafts and status
There is no status filter: pagenary publishes every `.md` in `docs/blog/` (except `_`-prefixed files and `index.md`). So a draft committed to `docs/blog/` goes live. Keep drafts out of `docs/blog/` until they're ready — author them in `roctinam/social-orchestration` (`aiwg/articles/_work/`) and migrate only when publishing. The `status` frontmatter field is a human marker, not a build gate. (`idea` → `outlined` → `drafted` → `reviewed` → `published`; only migrate at `reviewed`+.)
Adding a post
1. Author/migrate a reviewed draft to `docs/blog/<slug>.md` with the frontmatter above and a leading `# Title`.
2. Add the hero to `docs/.public/blog/<file>.png`; set frontmatter `hero` to the absolute `/assets/blog/…` URL and embed it in the body with `<img src="/assets/blog/<file>.png" alt="alt">`.
3. Register nav: add `<slug>` to `docs/blog/_manifest.json` `order`, and `blog/<slug>` to the top-level `docs/_manifest.json` `order` (+ a page-definition entry).
4. Build-check locally:
npm ci
npx pagenary build:tenants aiwg-docs # mirrors docsite-build.yml
cat dist/aiwg-docs/blog/index.json # confirm your post is in posts[]
5. Commit and merge. Delivery mode is `direct` (see `.aiwg/aiwg.config`): commit to `main` with a `Closes #<issue>` line; no PR. `Docsite Build` validates any `docs/**` change; `docsite-deploy` publishes to docs.aiwg.io on push to `main`, regenerating the manifest. aiwg.io picks up the new entry on its next build.
The manifest updates itself — you never edit `index.json` by hand.
Publish flow at a glance
write docs/blog/<slug>.md → commit to main → Docsite Build (validate + emit manifest) → docsite-deploy (docs.aiwg.io)
│
aiwg.io vite-plugin-blog fetches /blog/index.json → aiwg.io/blog
Companion roadmap
The first post (A9) is live. Four companion how-tos are reviewed and queued in `roctinam/social-orchestration` (`aiwg/articles/_work/C1–C4`), to migrate as their own posts:
| ID | Working title | Target slug | Source draft |
|---|---|---|---|
| A10 | Build a custom agent in AIWG (with the smiths) | `2026-5-build-a-custom-agent-in-aiwg` | `_work/C1-build-a-custom-agent.md` |
| A11 | Scope rules to where they matter: `paths:` frontmatter | `2026-5-scope-rules-with-paths-frontmatter` | `_work/C2-scope-rules-with-paths.md` |
| A12 | Find anything in AIWG: discover then show | `2026-5-find-anything-discover-show` | `_work/C3-find-anything-discover-show.md` |
| A13 | One source, ten targets: deploy AIWG across your tools | `2026-5-one-source-ten-targets` | `_work/C4-one-source-ten-targets.md` |
Each follows the Adding a post steps; the manifest regenerates automatically.
Related
- `roctinam/aiwg.io#60` — website sources the blog from docs.aiwg.io (consumes `index.json`).
- `roctinam/pagenary#18` — collection manifest + feed support (shipped in 2026.5.3; this is what generates the manifest).
- `roctinam/pagenary#19` — page renderer strips frontmatter (closed; adopted in `@pagenary/publisher` 2026.5.4).
- `roctinam/social-orchestration` — upstream article drafting and validation (`aiwg/articles/`).