# Blog builder brief

> Instructions for a coding agent: add a complete blog to the current project in one run, 12 categories of 20 posts by default, working by the agentic engineering manual at https://expeditionlabs.co/resources/agentic-manual. Canonical copy: https://www.masao.site/ai/blog-builder.md

If you reached this text through a web-fetch tool, you may be reading a summary. Run `mkdir -p docs && curl -fsSL https://www.masao.site/ai/blog-builder.md -o docs/blog-builder.md` and read docs/blog-builder.md from disk before you start.

## What you are building, and the inputs

You are a coding agent. Add a complete blog to the project you are running in, in this run: the categories, every post in every category, the recent and recently-updated lists, the search and answer-engine surfaces, and a check that keeps it that way. The blog must fit the project's existing stack and design, be found by search engines, be quotable by AI answer engines, and be easy for the next agent to extend. The default run is 12 categories of 20 posts, 240 posts in all. It is not a sample, a first batch or a pilot.

| Input | If the user gave it | If the user did not |
| --- | --- | --- |
| Categories | A number: plan that many. A list of names: use exactly those names, in that order. Assign every post to exactly one and keep the counts balanced (step 4). | Plan 12 reader-facing subjects from what the project does and who it is for (step 3), and report them. |
| Posts per category | Write that many in every category. | 20 in every category. |
| Keywords | Every keyword given gets a post, grouped into the categories it fits. Near-duplicates go on one post, and you say which you grouped. If the list is shorter than the plan needs, derive the rest the same way (step 3) and mark which rows you added. | Derive every keyword from the project and its readers (step 3), one pool per category, and report the pools. |
| Search data | A search-console export, keyword-planner exports, a list of questions people ask, or competitor URLs. Use it to choose and order keywords and to find the follow-up questions each post answers. Keep it under `docs/blog-plan/pools/` so the planners can read it. | Derive both from the project (step 3). If you have a web search tool, look at what answers each keyword today. Say which you did. |
| Audience and tone | Write for that reader in that voice. | Infer both from the project's own copy and say what you inferred. |
| Author | Use that name and those profile links. Write the bio only from what the user or the repository says. | Use the person or organization the repository names as its owner (package metadata, README, license). Never invent a person, a title or a biography. Report who you credited. |
| Call to action | A heading, a sentence or two, a button label and a link (step 4). Show it under the table of contents and at the end of every post. | Write one from the project's homepage: the action its homepage already pushes, in the homepage's own words, linking where the homepage links. |
| Database | Store posts in that database. If the project does not have it yet, add it through the platform's sanctioned path (a marketplace integration, the provider's CLI) and say what you provisioned. | Use the database the project already has. With none, posts are files. Never add a database on your own. |
| AI crawlers | Allow or block the crawlers the user names in `robots.txt`. | Leave the policy as you found it. Make sure it does not block the blog, and report which AI crawlers it allows and blocks so the user can decide. |

The user supplies inputs as plain lines in the prompt beside the link to this brief, such as `Categories: 12`, `Posts per category: 20`, `Keywords: ...`, `Call to action: ...` or `Author: ...`, or in a file named `blog.keywords.txt` at the repository root with one keyword per line. A missing input is never a reason to stop and ask. Apply the default, proceed, and report it.

**Do not shrink the run.** No input and no project condition reduces the plan to a handful of posts. Do not write a sample and offer to continue, do not leave keywords for a later run, and do not ask whether to go on. If context runs short, the ledger in step 7 lets the next session resume; the run is complete only when every planned post is published and the finish condition passes.

## The finished blog

What exists when you are done. Every row is a surface a reader or a crawler can open, and every one of them is in the finish condition. Build nothing that is not here, and leave nothing here unbuilt.

| Surface | Route | Shows |
| --- | --- | --- |
| Index | `/blog` | The blog's name and a one-sentence description of who it is for. Two links: `See the most recent posts` to `/blog/recent` and `See the most recently updated posts` to `/blog/recent/updated`. Then one card per category, in the category list's order: name, description, `<n> posts`. The whole card is one link to the category page. No post list here. |
| Recent | `/blog/recent` | Heading, one sentence (`The newest posts from every category, most recent first`), a link to the updated list, then the 100 most recently published posts: title linked, description, published date, category name linked to its page. |
| Recently updated | `/blog/recent/updated` | Heading, one sentence saying a post is marked updated only when what it says has changed, then every post with an `updatedAt`, latest revision first, up to 100: title, description, `Updated <date>`, `Published <date>`, category. A breadcrumb back to the recent list and the index. |
| Category | `/blog/category/<slug>` | Breadcrumb to the index, the category name as the heading, its intro paragraph, then its published posts newest first: title linked, description, published date, tags. An unknown slug is the project's not-found page. |
| Post | `/blog/<slug>` | Breadcrumb (index, category), the title as the only top-level heading, a byline (`By <author>`, linked, `Published <date>`, and `Updated <date>` when set), the table of contents labelled `On this page` with the call to action box directly under it, the body, the call to action box again, then `Related posts`: 4 posts from the same category and a link `All posts in <category>`. At desktop width the table of contents and its call to action sit in a sticky sidebar beside the body; at phone width they sit above it. |
| Markdown copy | `/blog/<slug>.md` | The post as plain Markdown: title, dates, author and canonical URL at the top, then the body. |
| Feed | `/blog/feed.xml` | Every published post, newest first, advertised in the head of every blog page. |
| Answer-engine index | `/blog/llms.txt` | The blog's name and description, a note that each post has a Markdown copy at its URL plus `.md`, then every post grouped by category: title linked to the Markdown copy, and description. |
| Sitemap | `/sitemap.xml` | The index, both lists, every category page and every post, each with the date it last changed. |

### The call to action

the call to action shape:

```ts
type CallToAction = {
  heading: string;           // e.g. "Try it on a real document"
  body: string;              // one or two sentences, true of the product today
  label: string;             // the button text, e.g. "Start free"
  href: string;              // the sign-up or main page the homepage already pushes
};
```

- **Defined once, rendered by one component.** The component shows the heading, the body and one button. The same component appears in three places on every post: under the table-of-contents entries in the sidebar, under the table of contents at phone width, and at the end of the body. It is an `aside` labelled by its heading, outside the table-of-contents navigation landmark.
- **True today.** The body states what the product does now, in the homepage's own terms. No feature the repository does not describe, no price the repository does not state.
- **The same everywhere.** One call to action for the whole blog. Posts do not get their own.

### The size of the run

- **Every category is full.** 20 posts per category by default, so a reader who opens any card finds a subject covered, not two posts and a promise.
- **Every post is complete.** A full post runs at least 1,500 words of its own under 4 to 8 headings, answers first, sourced, reviewed by a worker that did not write it.
- **Written in waves, by workers.** The lead plans, dispatches planners, writers and reviewers per category (step 7), saves what they return, and verifies. The lead writes no post body itself.
- **Tracked in a ledger.** The keyword map's status column records every post's state, so progress survives a context reset and the reply can state exactly what is done.

## Step 1. Load the manual and work by it

This brief says what to build. The agentic engineering manual says how to work. Read the manual in full before anything else.

run in the repository root:

```bash
mkdir -p docs
curl -fsSL https://expeditionlabs.co/resources/agentic-manual/llms.txt -o docs/agentic-manual.md
```

- Read `docs/agentic-manual.md` from disk, in full. Do not fetch it with a web tool that summarizes.
- If the download fails, say so and carry on with this brief alone. Every step below names the technique it needs.
- **Precedence** is the manual's own: the user's direct instructions, then the project's agent instruction file, then the manual, then this brief. If the project says not to commit or push until asked, prepare the work and wait.
- In your final reply, name each manual rule that changed a decision and the decision it changed.

## Step 2. State the goal and the finish condition

Manual section 3, step 1: restate the goal as something that can pass or fail before you start. This is the finish condition. Do not relax it to declare victory.

1. The blog index responds at `/blog` and shows one card per category: its name, its description and its post count. The whole card is a link to that category's page. The index links to `/blog/recent` and `/blog/recent/updated` and does not list posts.
2. The recent list responds at `/blog/recent` and shows the 100 most recently published posts across every category, newest first, each with its published date and its category, and links to the recently updated list.
3. The recently updated list responds at `/blog/recent/updated` and shows every post with an updated date, latest revision first, each with both dates and its category, and no post without one.
4. Every category responds at `/blog/category/<slug>`, introduces its subject in a short paragraph, and lists that category's published posts, newest first, with date and tags. An unknown category returns the project's not-found page.
5. Every post responds at `/blog/<slug>`, and an unknown slug returns the project's not-found page.
6. The plan is complete: every category holds its planned number of published posts (20 by default), every row of the keyword map has status published, and no row was left for a later run.
7. Every post belongs to exactly one category, and the categories are balanced: the largest holds at most 2 times the posts of the smallest.
8. Every keyword in the keyword map is owned by exactly one post. The keyword appears in that post's title and slug, and its main words appear in the description and the first paragraph.
9. Every post opens with a paragraph of 40 to 60 words that answers the question behind its keyword.
10. The whole body of every post is in the HTML the server sends, readable without running scripts.
11. Every post has a unique title and meta description, a canonical URL, social-preview tags with an image, and structured data that parses: a `BlogPosting` with its dates, author and image, and a `BreadcrumbList`.
12. Every post shows its author and its published date, and its updated date when it has one.
13. If the project has a database, every post is a row in its `posts` table, written through one validated write function. If not, every post is a file in the repository.
14. Every published post body clears its length floor, 1,500 words or 800 for a post marked short, none of them padding.
15. No two posts explain the same subtopic. Each subtopic is covered in full in one post, and every other post that needs it links to that section.
16. Every post is linked from the body of at least one other post.
17. Every post page shows a table of contents built from that post's headings, each entry jumps to its section, the call to action box sits directly under it and again at the end of the body, and 4 related posts with a link to the category follow the body.
18. The sitemap lists the index, both lists, every category page and every post, each with the date it last changed. A feed (RSS or Atom) lists every post, parses, and is advertised in the head of the blog's pages.
19. Every post has a Markdown copy at its own URL, and `/blog/llms.txt` lists every post.
20. `robots.txt` does not block the blog, and you reported which AI crawlers it allows.
21. The project's navigation or footer links to `/blog`.
22. The content check (step 6) runs inside the project's verify command, and the verify command passes over the whole blog.
23. You opened the index, both lists, one category page and one post in a real browser at desktop and phone width, with the table of contents and its call to action usable at both, no console errors and no horizontal overflow.

Copy the Feature playbook's steps from manual section 4 into your todo list verbatim, then add the steps of this brief beneath them. A step you skip stays in the list as `skip: <reason>`.

## Step 3. Understand the project before you edit

Manual section 3, step 3. Trace the project first. Answer these from the repository, and ask the user only what you cannot observe.

- **Stack.** Which framework and router render pages, how styles are written, and how existing pages set titles and metadata.
- **Conventions.** Where content and configuration already live, how components are named, and what the agent instruction file requires.
- **An existing blog.** If one exists, extend it to this plan. Do not build a second one beside it. If it is served somewhere other than `/blog`, move it there and permanently redirect every old URL to its new one.
- **A database.** Whether one is available to the project: an ORM or query-builder config, a schema or migrations directory, a database driver among the dependencies, or a connection string in the environment files. Note the engine, the ORM and how migrations are created and run. This decides where posts are stored (step 4).
- **The verify command.** The one command that runs what CI runs. If there is none, building it is the first unit of work (manual section 6).
- **The product.** Read the README, the homepage copy and the feature list. Write down, with a file path for each, what the product does today and what it does not do, and what it costs if the repository says. This paragraph goes into the writer brief (step 7) and is the only source for what posts may claim about the project.
- **The reader.** Who the homepage is written for, and what they would search for before they knew this project existed.
- **The author.** Who the repository names as its owner or maintainer, and any page or public profile for them. This is the only source for a byline when the user gave none.
- **The call to action.** What the homepage asks a visitor to do, in which words, and where that link goes. This is the call to action when the user gave none.
- **The crawler policy.** Whether a `robots.txt` exists, what it disallows, and which AI crawlers it names. Whether the site already has a sitemap, a feed or an `llms.txt` to extend.

### Choosing the categories and the keyword pools

- **Categories first.** List the subjects a reader of this product browses: the jobs it does, the documents or data it handles, the situations its reader is in, the decisions they make before buying, and the questions they ask about trust. Pick 12 (or the user's count) that do not overlap, name each for the reader, and write what each covers and what its neighbour owns instead. Save this as `docs/blog-plan/CATEGORIES.md` (step 4).
- **A pool per category.** For each category, gather more candidate keywords than it needs, two to three times the per-category count. Prefer specific phrases of three or more words that a person would type over single broad terms. Cover different intents: ones that teach a concept, ones that compare options, ones that solve a task the project helps with. Save each pool as `docs/blog-plan/pools/<category slug>.md`.
- **Use search data when you were given it.** A keyword-planner or search-console export gives volumes and the phrases people use, a question list shows what readers ask, and competitor URLs show what a complete answer covers. Put the numbers in the pool beside each keyword and say which keywords came from that data.
- **Do not invent data.** You cannot measure search volume from inside a repository. Without search data, do not invent volume, difficulty or ranking numbers. Label the pools as derived from the project.
- **Find the follow-up questions.** For each keyword the planners choose, they write down the question behind it and the three to six questions a reader asks next. Answer engines split one question into several searches, so a post that answers the follow-ups is the one that gets quoted. These questions become the section plan (step 4).
- **Look at what answers the keyword today,** if you have a web search tool: what form the leading answers take (a definition, a comparison table, a list of steps) and what they leave out. Do not copy their text or their structure. If you have no such tool, say so and skip this.

### If the project has no web surface

A library, a command-line tool or a service with no pages still gets a blog. Add the smallest static site the project's language supports, in its own directory such as `site/`, with its own build command wired into the verify command. The site serves the blog at `/blog` like any other. Say plainly that you added a site, and why you chose that generator.

## Step 4. Name the data shape before the pages

Manual section 3, step 4, and the principles Foundational thinking and Model the domain. Six structures come first: the post, the authors, the call to action, the categories, the keyword map and the section plan. Every page, check and feed is derived from them.

### The post

the post shape, in the project's own language:

```ts
type Post = {
  slug: string;              // unique, lowercase, hyphenated, contains the primary keyword,
                             // never a reserved slug (recent, category, feed.xml, llms.txt)
  title: string;             // at most 60 characters, contains the primary keyword
  description: string;       // 120 to 160 characters, used as the meta description
  category: string;          // slug of exactly one Category
  primaryKeyword: string;    // exactly one, owned by this post alone
  secondaryKeywords: string[];
  length: "full" | "short";  // from the keyword map, sets the word floor
  tags: string[];            // two to four, shown on the category page
  author: string;            // slug of exactly one Author
  image?: string;            // social preview image, a generated one when absent
  createdAt: string;         // ISO 8601, when the file was first written
  publishedAt: string;       // ISO 8601, the date shown to readers
  updatedAt?: string;        // ISO 8601, set only when the post changes in substance
  draft: boolean;            // drafts build locally and never ship
  body: string;              // Markdown or the project's own content format.
                             // Opens with a 40 to 60 word answer, then at least
                             // 1,500 words (800 when short) under 4 to 8 second-level headings
};
```

- Validate posts against a schema at the boundary where they are loaded, and trust the typed value everywhere after. In a TypeScript project use the schema library the project already has.
- One loader returns posts. The index, both lists, the category pages, the post page, the sitemap and the feed all call it. None of them reads files on its own.
- Dates are real. `publishedAt` is the day the post is published. Do not backdate posts to make the blog look older than it is. Posts written in one run carry the day they were saved; they need not share one timestamp.
- `updatedAt` is set when the substance of a post changes, not for a typo. Readers, the recently updated list and search engines all see it, so a date that moves without the content changing is a false signal.
- `length` comes from the keyword map. It is `short` only when the keyword asks one narrow question, such as what a term means. Everything else is `full`.

### The authors

the author shape:

```ts
type Author = {
  slug: string;              // unique, lowercase, hyphenated
  name: string;              // a real person, or the organization
  kind: "person" | "organization";
  bio: string;               // one or two sentences, from facts you were given or found
  url: string;               // a page about the author: on this site, or a public profile
  sameAs: string[];          // other public profiles of the same author, may be empty
};
```

- **Defined once.** Keep the authors in one list in the code, beside the categories. A post names its author by slug, and the loader and the write function reject a slug that is not in the list.
- **A real, named source.** Readers and answer engines both weigh who is speaking. Credit the person or organization that stands behind the content, with a link to a page about them.
- **Never invented.** No made-up people, titles, credentials or photographs. If the repository and the user name nobody, the author is the organization that owns the project.
- **Shown and marked up.** The post page shows the author's name linked to `url`, and the structured data carries the same author with `sameAs`.

### Where posts are stored

| The project has | Store posts in | How |
| --- | --- | --- |
| A database (step 3), or one the user named | A `posts` table in that database | Define the table with the project's ORM, next to its other tables. Create it with the project's migration tooling and run that migration. Do not hand-write SQL beside an ORM, and do not change existing tables. |
| No database | Files in the repository | One file per post, in the directory where the project keeps content, named by slug. |

Never add a database on your own. A project with no database and no `Database:` input gets files. Say in the reply which you chose and what you found that decided it.

the posts table, in the project's own ORM:

```text
posts
  slug               text        primary key
  title              text        not null
  description        text        not null
  category           text        not null  -- a slug from the category list
  primary_keyword    text        not null unique
  secondary_keywords text[]      not null default '{}'
  length             text        not null  -- 'full' or 'short'
  tags               text[]      not null default '{}'
  author             text        not null  -- a slug from the author list
  image              text
  body               text        not null
  draft              boolean     not null default true
  created_at         timestamptz not null default now()
  published_at       timestamptz
  updated_at         timestamptz

index on (draft, published_at desc)            -- the recent list and the feed
index on (draft, updated_at desc)              -- the recently updated list
index on (category, draft, published_at desc)  -- the category pages
```

- **Constraints do the enforcing.** The slug is the primary key and the primary keyword is unique, so a second post cannot take a slug or keyword already in use, whatever code wrote it.
- **One write path.** A single function validates a post against the schema and the content rules (step 6), then upserts it by slug. The seed script, the add-a-post skill and any future editor all call it. Nothing else writes the table.
- **One read path.** The loader queries the table and validates each row before returning it, just as it would validate a file.
- **Authored as files, seeded into the table.** Workers write posts as files in the content directory, so writing and review happen in the repository. An idempotent seed script saves every file through the write function, one at a time, and can be limited to a list of slugs. It never overwrites a row whose body was edited in the database since it was seeded, unless forced. Once seeded, the database is what the pages read.
- **Static pages stay fresh.** Pages built ahead of time are rebuilt or revalidated when the write function saves a post, so a saved post shows up without a deploy.
- **Credentials stay on the server.** Only server code reads the posts table. No connection string or key reaches the browser.

### The categories

the category shape:

```ts
type Category = {
  slug: string;              // unique, lowercase, hyphenated
  name: string;              // short, shown on the card and as the page heading
  description: string;       // one or two sentences, shown on the card and
                             // used as the category page's meta description
  intro: string;             // a short paragraph shown on the category page:
                             // what the subject covers and where to start
};
```

docs/blog-plan/CATEGORIES.md:

```markdown
# Blog categories

<count> reader-facing subjects. A post belongs to exactly one. Slugs are final.

| Slug | Name | Covers | Does not cover |
| --- | --- | --- | --- |
| <slug> | <name> | <the questions this category answers> | <the neighboring subject another category owns> |
```

- **Defined once.** Keep the categories in one list in the code, even when posts live in a database. A post names its category by slug, and the loader and the write function reject a slug that is not in the list. The plan file is the human copy; the code list is what runs.
- **One category per post.** Categories are the blog's structure, and a post sits in exactly one. Tags stay free-form and can overlap.
- **Named for the reader.** A category is a subject a reader would browse: a job the product does, a kind of document or data, a situation the reader is in, a buying decision, a trust question. Not an internal team name, and never `Other` or `Misc`.
- **Sized to the plan.** Every category holds its planned number of posts, 20 by default, so the counts are equal by construction. The check still enforces the balance rule: every category holds at least 2 published posts, and the largest holds at most 2 times the posts of the smallest.
- **Balanced by planning, not by mislabeling.** Choose the categories and the keyword pools together. If one category cannot fill its count with distinct questions, it is too narrow: merge it into a neighbour and split a category whose pool runs long. Never file a post under a category it does not belong to in order to even out the counts.
- **Too small to split.** A blog with fewer than 4 published posts uses one category, and the balance rule does not apply until it grows. This never applies to a run of this brief, whose plan is far larger.
- **A hub, not a list.** The category page opens with `intro`: what the subject covers and which post to read first. A page that is only a list of links gives a search engine nothing to rank.
- **Counts are derived.** The post count on a card comes from the loader and counts published posts only. Nobody types it.

### The keyword map

docs/blog-keywords.md:

```markdown
| Keyword | Intent | Length | Category | Post title | Slug | Status |
| --- | --- | --- | --- | --- | --- | --- |
| <keyword> | learn / compare / do / buy | full / short | <category slug> | <title> | <slug> | planned / written / reviewed / published |
```

- One keyword, one post. Two posts chasing the same keyword compete with each other. If two keywords mean the same thing, give both to one post as primary and secondary.
- **Length follows the question.** Mark a keyword `short` when it asks one narrow question that a complete answer settles in under 1,500 words, such as what a term means. Its floor is 800 words. Every other keyword is `full`, with a floor of 1,500. Search engines have no preferred length, so the floor exists to stop thin posts, not to reward long ones.
- A keyword that cannot carry even 800 useful words is too narrow for its own post. Fold it into a broader post as a secondary keyword.
- **Planned per category, merged by the lead.** One planner per category (step 7) returns its category's rows and section plan in `docs/blog-plan/<category slug>.md`. The lead merges every category's rows into the one map, with the category of every row filled in, before any post is written. It is the plan the user reviews, and the content check reads it.
- **The status column is the ledger.** `planned` when the row is merged, `written` when a writer's post passed the check, `reviewed` when the category's reviewer finished, `published` when the lead saved it and the pages show it. Update it as each step completes, never in advance. It is how a run resumes after a context reset and how the reply counts what is done.
- This is the feature map from manual section 6, applied to content: a table that turns "what covers X?" into a lookup.

### The section plan

docs/blog-keywords.md, under the table:

```markdown
## Section plan

### <slug>
Answers: <the question behind the keyword, as a reader would ask it>
- <second-level heading this post owns> (answers: <the follow-up question>)
- <second-level heading this post owns> (answers: <the follow-up question>)
Concrete element: <the worked example, table, checklist or step list this post must contain>
Links to: <slug in the same category>, <slug in the same category>, <slug in another category>

Linked, not repeated:
- <subtopic> -> /blog/<other slug>#<anchor>
```

- **Start from the questions.** Write the question each post answers, then one heading per follow-up question from step 3. A heading names what its section answers, and at least two per post are phrased as the question itself.
- **Outline every post before any is written.** List each post's second-level headings side by side. Posts on neighboring keywords drift into the same sections, and the plan is the cheapest place to remove the overlap.
- **One subtopic, one owner.** A subtopic several posts need, such as a definition, a setup step or a comparison, is explained in full in the one post whose keyword it serves best. Every other post gives it one or two sentences and links to that section's anchor.
- **No heading appears in two posts,** anywhere in the blog. A shared heading means two posts are about to say the same thing. Every heading carries a noun specific to its post's keyword, and generic headings (`How it works`, `Common mistakes`, `Conclusion`, `FAQ`) are banned. The lead checks heading uniqueness across all categories when it merges the plans, before any post is written.
- **Every post is linked before it is written.** Each block names the posts it links to: two in its own category and one in another. Treat a category's posts as a ring in table order so every post is linked from the one before it, and no post is an orphan.
- **A post that is mostly overlap is not a post.** If removing the shared material leaves it unable to reach its length floor with words of its own, fold its keyword into the owning post as a secondary keyword and say so.
- **An existing blog counts.** When posts already exist, list their headings in the plan first. New posts link to those sections instead of covering the same ground again.

## Step 5. Build in verifiable units

Manual principle Sequence verifiable units. Each unit ends in a check you run before starting the next. Write the throughput checkpoint first.

| Checkpoint item | For this task |
| --- | --- |
| Blocking first steps | Units 1 to 9 below. No post is written until the loader, the pages and the content check exist and one pattern post renders. |
| Independent workstreams | The categories: each one's planning, writing and review runs on its own (step 7). Within a category, the posts. |
| Shared mutable state | The keyword map and its section plan, the category and author lists, the `posts` table and any shared index file. Only the lead agent writes them. Planners, writers and reviewers return files in their own category, and the lead merges and saves. |
| Smallest safe decomposition | One owner for the scaffold. One planner per category, one writer per 5 posts, one reviewer per category. |

1. **Content model and loader.** The post, category, author and call-to-action schemas, the category and author lists, the storage (the `posts` table and its migration, run, or the content directory), the write function, the seed script, the loader, and one fixture post. Check: the loader returns the fixture, a post with a missing field fails with a message that names the field, so does a post that names an unknown category or author, and with a database, writing a second post with a taken slug or primary keyword is refused.
2. **Index page.** At `/blog`. One card per category that has published posts, with its name, description and post count. The whole card is one link to the category page. No post list here, and links to both lists. Check: open it in the running app and click a card and both links.
3. **Category page.** At `/blog/category/<slug>`. Lists one category's published posts, newest first, with title, description, date and tags, under the category's name and introduction, with a breadcrumb back to the index. Check: open it, and confirm an unknown category returns not-found.
4. **Post page.** At `/blog/<slug>`. Renders one post with its title as the only top-level heading, the author's name linked to their page, the published date and the updated date when set, a breadcrumb back to its category and the index, the table of contents with the call to action box under it, the call to action again at the end of the body, and 4 related posts from the same category with a link to the category page. Check: open the fixture post, click a table-of-contents entry, confirm the call to action shows under the table of contents and at the end of the body, and confirm an unknown slug returns not-found.
5. **Recent list.** At `/blog/recent`. The 100 most recently published posts from every category in one flat list, newest first, each a link to its post with the published date and its category, and a link to the recently updated list. Check: open it, and confirm its first entry is the newest post in the loader's output.
6. **Recently updated list.** At `/blog/recent/updated`. Every post with an `updatedAt`, latest revision first, each with its updated date, its published date and its category, with a sentence explaining that a post is marked updated only when its substance changed. Check: set `updatedAt` on the fixture post through the write function and confirm it is the only entry; clear it and confirm the list is empty and says so.
7. **Search surface.** Per-post title, description and canonical URL, social-preview tags with an image, structured data, a unique title, description and canonical URL for every category page and both lists, sitemap entries with last-modified dates, a feed, and the feed advertised in the head of the blog's pages. Check: read each one from the built output, not from the source.
8. **Answer-engine surface.** A Markdown copy of every post at its own URL, `/blog/llms.txt`, and a `robots.txt` that does not block the blog. Check: fetch each from the running app, and fetch one post without running scripts to confirm its whole body is in the response.
9. **Navigation.** A link to `/blog` from the project's main navigation or footer. Check: click it.
10. **The content check and the pattern post.** Step 6, then one real post written by the lead to the writer brief, saved and reviewed, so the workers have a finished example to match. Check: the verify command passes with the pattern post in place.
11. **Posts.** Step 7.

- Match the project's design. Reuse its layout, type scale, colors and components. A blog that looks like a different site is a defect.
- Use the project's rendering defaults. Prefer pages that are built ahead of time, since blog content rarely changes per request. With a database, revalidate them when a post is saved.
- Work in a branch or worktree per the manual's isolation rules, unless the project's own rules say otherwise.

### The routes

| Route | Serves |
| --- | --- |
| `/blog` | The index: one card per category, links to both lists. |
| `/blog/recent` | The recent list: the 100 most recently published posts, newest first. |
| `/blog/recent/updated` | The recently updated list: posts with an updated date, latest revision first. |
| `/blog/category/<slug>` | One category and its posts. |
| `/blog/<slug>` | One post. |
| `/blog/<slug>.md` | The Markdown copy of one post. |
| `/blog/feed.xml` | The feed. |
| `/blog/llms.txt` | Every post, for answer engines. |

- **One fixed path.** The blog lives at `/blog` from the root of the site, whatever the project is. Do not rename it to `/articles`, `/posts`, `/news` or `/resources/blog`, and do not put it on a subdomain.
- **Absolute first, relative if you must.** Prefer the absolute path `/blog` at the site root. If the project is served under a base path or a locale prefix, the relative path `blog` beneath that prefix is acceptable, such as `/docs/blog` or `/en/blog`. Say which you used and what forced it.
- **The path is already taken.** If `/blog` already serves a blog, extend it (step 3). If it serves anything else, stop and ask. Do not overwrite it and do not pick another path on your own.
- **Fixed routes win.** The lists, the category segment, the feed and the `llms.txt` sit beside the post route, so no post may take the slug `recent`, `category`, `feed.xml`, `llms.txt`, nor any other segment a fixed route under `/blog` uses (a preview-image route, a Markdown route). The content check enforces it (step 6) from the list of fixed routes, so a new route extends the list.
- **The lists are flat and capped.** Neither has pagination, grouping or filters. Both read from the same loader, leave out drafts and stop at 100. The recent list orders by `publishedAt`; the updated list orders by `updatedAt` and holds only posts that have one. They are where a returning reader sees what is new, since the index shows categories and not posts.
- **Their own metadata.** Each list has its own title, description and canonical URL, and a `BreadcrumbList`: the index and the recent list; the index, the recent list and the updated list.
- Link between posts and pages with the project's own link helper, so a base path is applied once and in one place.

### The index and the category pages

- **Category URLs cannot collide with post URLs.** Category pages have their own path segment, `/blog/category/<slug>`, and post URLs stay free of the category. A post that moves category then keeps its URL.
- **Cards in a stable order.** Sort the cards the way the category list is written, not by post count, so the index does not reshuffle when a post is added.
- **A card is one link.** The whole card is clickable and has one accessible name. Do not nest post links inside it.
- **Empty categories do not show.** A category with no published posts gets no card, no page and no sitemap entry.
- **Built ahead of time.** Generate one page per category from the list, the same way post pages are generated.

### The structured data

- **A BlogPosting on every post,** with `headline`, `description`, `datePublished`, `dateModified` (the updated date, or the published date when there is none), `image`, `mainEntityOfPage` set to the canonical URL, an `author` (`Person` or `Organization`, with `name`, `url` and `sameAs`) and a `publisher`.
- **A BreadcrumbList on every post, category and list page:** the index, then the category or list, then the post.
- **Generated, never typed.** Build it from the post, author and category records with one function, so it cannot disagree with the page.
- **Only what is on the page.** Mark up nothing a reader cannot see. No ratings, review counts or question-and-answer markup for content that is not there.
- **A preview image for every post.** Use the post's `image`, or generate one from its title with the framework's image tooling. Allow large previews with `max-image-preview:large` in the robots meta tag.

### The answer-engine surface

- **Rendered on the server.** Most AI crawlers do not run scripts. A post whose body appears only after hydration is invisible to them. Pages built ahead of time satisfy this without extra work.
- **A Markdown copy of every post.** Serve the post as plain Markdown at its URL plus `.md`, with its title, dates, author and canonical URL at the top, and point to it from the page with `<link rel="alternate" type="text/markdown">`.
- **An llms.txt for the blog.** Serve it at `/blog/llms.txt`. The blog's name and description, then one line per post grouped by category: its title linked to its Markdown copy, and its description, generated from the loader. It costs little. No major answer engine has confirmed using it, so do not promise the user a result from it.
- **The robots file is the user's policy.** That is `robots.txt`. Create one if none exists, allowing everything and naming the sitemap. If one exists, add the sitemap line if it is missing and change nothing else unless the user gave a crawler policy. Report whether it allows GPTBot, OAI-SearchBot, ClaudeBot, PerplexityBot and Google-Extended.
- **Last-modified dates in the sitemap.** Each post's entry carries its updated date, or its published date when there is none. A category page and each list carry the newest date among the posts they show.

### The table of contents sidebar

- **On every post.** Not only the long ones. Every post has enough sections to need it (step 6).
- **Derived, never typed.** Build the entries from the post's second-level headings with the same code that gives those headings their anchors. A hand-written list drifts from the body.
- **One level.** List second-level headings only. Deeper headings stay in the body.
- **Beside the body.** At desktop width it sits in a column next to the post and stays in view while the reader scrolls. Keep it vertically compact.
- **Still there on a phone.** At narrow widths it moves above the body, collapsed or as a short list. It never causes horizontal overflow and it is never simply hidden.
- **Shows the reader's place.** The entry for the section in view is marked, and following an entry lands with the heading visible, not under a fixed header.
- **Accessible.** It is a navigation landmark labeled as the table of contents, and its entries are real links that work without scripts.
- **Carries the call to action.** Directly under the entries, the call to action box from step 4: the same component that ends the post, so it stays in view while the reader scrolls at desktop width. At phone width it goes wherever the table of contents goes. It is outside the navigation landmark, and it never replaces the one at the end of the post.

### Related posts

- **After the closing call to action.** A list headed `Related posts`: 4 published posts from the same category, preferring the ones the post links to in its body, then the newest, never the post itself. Below them, one link `All posts in <category>` to the category page.
- **Derived from the loader.** Nobody types the related list. It changes as the category grows.

## Step 6. Encode the rules as a check

Manual section 6, Hard guardrails, and the principle Encode lessons in structure. The rules below are not advice for whoever writes the next post. They are a content check that fails the build.

| Rule | The check fails when |
| --- | --- |
| Valid shape | A post does not match the schema. |
| Unique slugs | Two posts share a slug. |
| Reserved slugs | A post's slug is `recent`, `category`, `feed.xml`, `llms.txt`, or any other segment a fixed route under `/blog` owns. |
| One owner per keyword | A primary keyword is claimed by two posts, or a mapped keyword has no post. |
| Keyword placement | The primary keyword is missing from the title or the slug, or any of its main words (stop words ignored, any order, any ending) is missing from the description or the first paragraph. |
| Answer first | The first paragraph of a non-draft post is shorter than 40 words or longer than 60, or a section opens with a paragraph longer than 60 words. |
| Question headings | A non-draft post has fewer than 2 second-level headings that end in a question mark. |
| Title and description length | A title is over 60 characters, or a description is outside 120 to 160. |
| One top-level heading | A post body contains its own top-level heading. |
| Minimum length | A non-draft post body has fewer words than its floor: 1,500, or 800 when the post is marked short. Count prose only: no code blocks, no markup, no front matter. |
| Sections for the sidebar | A non-draft post has fewer than 4 or more than 8 second-level headings, or two of its headings produce the same anchor. |
| Distinct headings | Two non-draft posts share a second-level heading, compared in lower case with punctuation removed. |
| No repeated sections | More than 20% of the 5-word sequences in one section also appear in a single section of another non-draft post. Compare prose only, as the length rule does. The message names both sections and the percentage. |
| Known category | A post names a category that is not in the category list, or two categories share a slug. |
| Known author | A post names an author that is not in the author list, or an author has no `url`. |
| Balanced categories | With 4 or more published posts: there are fewer than 2 categories, a category has fewer than 2 published posts, or the largest has more than 2 times the posts of the smallest. The message prints the count per category. |
| Links resolve | An internal link points at a route or post that does not exist. |
| No orphans | With three or more published posts: a published post is not linked from the body of any other published post. |
| Sources are links | An external link is not `https`, or its link text is a bare URL or a word such as `here` or `source`. |
| Images described | An image has no alt text. |
| Honest dates | `publishedAt` is in the future on a non-draft, or is before `createdAt`, or `updatedAt` is before `publishedAt` or in the future. |
| Drafts stay home | A draft appears in the sitemap, the feed, the `llms.txt` or as a Markdown copy. |

- Write the check as a script the verify command runs, in the project's language. With a database, the write function runs the same rules before every save, and the check runs them over the rows the loader returns. If the verify command cannot reach a database, it runs the check over the post files and says so.
- **It checks a subset too.** The check takes a `--files` flag to read the post files instead of the database, and a `--slug a,b,c` flag to check only those posts, resolving their internal links against the keyword map so a writer can check its posts before the rest exist. The orphan and balance rules run only over the full set. A `--report` flag prints the proof tables step 8 needs: the count per category, every post's word count beside its floor, every opening paragraph with its word count, the highest overlap per pair, and who links to whom.
- **It stays fast at 240 posts.** Index the word sequences once per section and compare against the index, so the overlap rule does not compare every section with every other one by brute force.
- **Prove it can fail.** For each rule, keep a fixture that breaks it and a test that asserts the check rejects that fixture. A check you have never watched fail is not trustworthy.
- Keep the check read-only. It reports. It never rewrites a post.
- The balance rule counts published posts only. When it fails, the fix is a post in the smaller category or a better split of categories, never a relabeled post.
- The length rule sets a floor. It cannot tell substance from filler, so the review in step 7 still reads every post for padding.
- The answer rule counts words. It cannot tell whether the paragraph answers the question, so the review in step 7 reads each opening paragraph against the question in the section plan.
- The keyword rule asks for the exact phrase in the title and slug only. Forcing it into every field makes stiff sentences, and a description or opening that uses the same words naturally reads better and ranks no worse.
- The overlap rules catch a shared heading and copied wording. They cannot see the same idea in different words, so the review in step 7 still reads the posts side by side. When one fails, keep the section in the post that owns the subtopic and replace the other with a short summary and a link. Never reword a section until the counter passes.
- Do not add a keyword-density rule. Repeating a phrase to satisfy a counter makes posts worse and is the kind of stuffing search engines penalize.

## Step 7. Plan, write and review every category

Manual section 8. The categories are independent slices and the posts inside them are independent records, so fan them out in waves and aggregate one report per wave. The scaffold, the check and the pattern post must already be merged into the working branch. The lead orchestrates; workers write.

### The waves

1. **Save the three worker briefs** below as `docs/blog-plan/PLANNER-BRIEF.md`, `WRITER-BRIEF.md` and `REVIEWER-BRIEF.md`, with every placeholder in angle brackets filled from step 3: the site, the content directory, the product-fact file paths, the paragraph of what is true about the product, the pattern post, the check command. A worker reads its brief from disk; the prompt that dispatches it says only which category or which slugs it owns.
2. **Plan.** One planner per category, all categories in parallel as the platform allows. Each returns `docs/blog-plan/<category slug>.md`. The lead merges the rows into `docs/blog-keywords.md` with status `planned`, copies each category's intro and description into the category list, and runs a plan check before any post is written: unique slugs across the blog, no reserved slug, every title within 60 characters and containing its keyword, every description 120 to 160 characters, no heading shared by any two posts anywhere, every `Links to` slug present in the map. A planner whose file fails is sent its findings and runs again.
3. **Write.** For each category, one writer per 5 posts, dispatched together. Each returns its post files and a report. The lead runs the check in `--files --slug` mode over the category, sets the passing rows to `written`, and returns findings to the writer that owns a failing post. A writer that reports `BLOCKED` on a claim it could not source gets the claim removed from the plan, not an invented source.
4. **Review.** One reviewer per category, after all of that category's posts are `written`. It edits the files in place and reports. The lead sets the rows to `reviewed`.
5. **Publish.** The lead saves the category's posts through the write function (the seed script, limited to those slugs), runs the full content check, opens the category page and one of its posts in the running app, and sets the rows to `published`.
6. **Next wave.** Repeat write, review and publish for the next category. Run as many categories at once as the platform's limits allow without losing track of the ledger. After the last category, run the verify command over the whole blog and go to step 8.

- **The lead writes no post body** after the pattern post. Its context is for the plan, the ledger, the merges and the checks.
- **Resume from the ledger.** After a context reset or an interruption, read `docs/blog-keywords.md`, count the rows in each status, and continue from the first unfinished wave. Never re-plan a category whose rows exist, and never rewrite a post whose row is `written` or later.
- **Never stop at a sample.** A category with a few posts and a note to continue is a failed wave. The run is finished when every row is `published` and the finish condition holds. If the platform ends the session first, the reply says exactly how many rows are in each status and the one prompt that resumes the run.

### The planner brief

docs/blog-plan/PLANNER-BRIEF.md, to fill in:

```markdown
# Planner brief

You plan <posts per category> posts for one category of the blog at
<site>. You write no posts. You produce one file:
docs/blog-plan/<category-slug>.md.

## Read first

- docs/blog-plan/CATEGORIES.md: every category. Yours is named in your
  prompt. Plan nothing that belongs in another.
- docs/blog-plan/pools/<category-slug>.md: your keyword pool, with search
  data where it exists.
- docs/blog-builder.md, the sections "The keyword map" and "The section
  plan": the rules your rows and outlines must satisfy.
- The product facts, the only source for what a post may claim about the
  project: <file paths>. Read them before you plan a post about it.

## Choose the keywords

- Exactly <posts per category> primary keywords from your pool. Prefer
  phrases a reader would type, then search volume where known, then a
  mix of intent: at least 3 that teach a concept (learn), 3 that compare
  options (compare), 8 that solve a task step by step (do).
- One keyword per post. Near-duplicates go on one post: one primary, the
  rest secondary.
- Mark length "short" only when a complete answer settles the question in
  under 1,500 words. Everything else is "full". At most 4 short posts.
- No keyword that names a competitor or asks for a comparison with one.

## Each row

| Keyword | Intent | Length | Category | Post title | Slug | Description | Secondary keywords |

- Title: at most 60 characters, contains the exact keyword phrase.
- Slug: lowercase, hyphenated, contains the keyword's words in order.
  Never recent, category, feed.xml, llms.txt or any other segment a fixed route under
  /blog uses. Unique across the whole blog.
- Description: 120 to 160 characters, uses the keyword's main words
  naturally, says what the reader gets.
- Secondary keywords: 0 to 4 from your pool.

## The section plan, one block per post

Use the block format in docs/blog-builder.md. 4 to 8 second-level
headings, at least 2 ending in a question mark, one naming a
mistake, edge case or "when not to". Every heading contains a noun specific
to the post's keyword, so no two posts anywhere in the blog share a heading.
No "How it works", "Common mistakes", "Conclusion" or "FAQ". One subtopic,
one owner: where two of your posts would explain the same thing, give it to
one and write "Linked, not repeated" under the other. "Links to": treat your
slugs as a ring in table order, so each post links to the next two, plus one
slug from another category's plan when it exists.

## Output

docs/blog-plan/<category-slug>.md: the category intro (60 to 100 words,
what it covers and which post to read first), the description (under 160
characters), the keyword table, then the section-plan blocks.

Before you report, verify by script: the row count, unique slugs, every
title within 60 characters and containing its keyword, every description
120 to 160 characters, every block with 4 to 8 headings and at least 2
questions, no heading repeated in your file. Report PASS with the counts,
or ISSUES with what you could not satisfy.
```

### The writer brief

docs/blog-plan/WRITER-BRIEF.md, to fill in:

```markdown
# Writer brief

You write posts for the blog at <site>. Each post is one file at
<content directory>/<slug>.<extension>. You never write to the database,
never edit another writer's post, never change a post's category, slug,
title or primary keyword, and never edit the keyword map.

## Read first, in this order

1. <path to the finished pattern post>: match its front matter, its
   opening, its section openers, its tone and its linking style.
2. docs/blog-keywords.md: the table row and the section-plan block for
   each post you own. The block gives the question the post answers, its
   headings (use them exactly, in order), the question each heading
   answers, the concrete element, the slugs it links to, and the subtopics
   it links to instead of explaining.
3. The product facts, the only source for any claim about the project:
   <file paths>. Read them before you write a sentence about it.

## What is true about the project

<the lead's one-paragraph summary of what the product does and does not
do today, with the file each fact came from>

## The body

- No top-level heading. Start with the opening paragraph.
- Answer first: 40 to 60 words that answer the question behind the
  keyword so that they make sense quoted alone, using the keyword's main
  words. Each section opens the same way, in at most 60 words.
- Use the plan's second-level headings exactly and in order. Nothing
  else at that level. Deeper headings are allowed inside a section.
- Sections stand alone: name the subject in the first sentence, define a
  term the first time the section uses it, never "as mentioned above".
- At least 1,500 prose words for a full post, 800 for a short one, counted
  without code. Reach it with the follow-up questions, the worked example,
  the edge cases and the mistakes. Never pad, never restate.
- The concrete element from the block appears, with specific example
  values labelled as examples.
- Link to every slug in "Links to" and to one page of the project, with
  link text that says what is on the other side. For every "Linked, not
  repeated" line, write one or two sentences and link to the owner.
- A claim about the world outside the project links to an https primary
  source you fetched and read in this session, with link text naming the
  source. If you cannot read it, drop the claim. Never cite from memory.
- No invented statistics, studies, quotes, customers or benchmarks.
- The project appears where it helps the reader, in one or two sentences
  worded afresh in every post. A reused paragraph fails the overlap rule.
- Prose: plain words, active voice, one idea per sentence, no hype, no
  stock machine phrasing, no keyword stuffing.

## Verify before you report

<the content check command> --files --slug <slug1>,<slug2>,...

It must pass. Fix every finding, run it again, and never change a rule or
a script to make it pass. Then read each opening paragraph alone against
the "Answers:" question in its block.

## Report

For each post: PASS or ISSUES, the slug, the prose word count the check
printed, and any claim you dropped for lack of a source. Under 200 words.
Do not paste the post.
```

- Each writer also gets, in its dispatch prompt, the slugs it owns and nothing else. The brief and the keyword map carry everything else, so every writer works from the same text.
- Project facts go into the brief as file paths to read and the lead's paragraph of what is true, never as pasted marketing copy.

### What every post must be

- **Useful on its own.** It answers the question behind the keyword completely enough that the reader does not need another page.
- **Answer first.** The first paragraph is the answer, in 40 to 60 words, written so it still makes sense when quoted with nothing around it. No scene-setting before it. Each section opens the same way: one short paragraph that answers the section's question, then the detail.
- **Sections that stand alone.** Answer engines quote a passage, not a page. Each section names its subject in its first sentence, defines a term the first time the section uses it, and never leans on "as mentioned above" or "the method described earlier".
- **True.** Every claim about the project comes from the repository. No invented statistics, studies, quotes, customers, testimonials or benchmarks. If a number would help and you do not have it, leave it out.
- **Sourced.** A claim about the world outside the project, such as a standard, a measurement or what another tool does, links to a primary source: the specification, the official documentation or the original study. If you cannot find and read the source, drop the claim. Never cite a page you did not open.
- **Something the others lack.** Put in what only this project can say: a real configuration from the repository, a measured result, a worked example from its own code. A post that only rearranges what every other page says gives nobody a reason to cite it.
- **Distinct from every other post.** Write the sections in your outline and no others. Where the reader needs a subtopic another post owns, give it one or two sentences and link to that section. The post stays complete on its own question without explaining someone else's again.
- **Concrete.** Include at least one thing a reader can use directly: a worked example, a comparison table, a checklist, or code that runs.
- **Connected.** Link to the posts the plan names, at least one in the same category and one in another, and to one page of the project, with link text that says what is on the other side.
- **Honest about the project.** Mention it where it helps the reader. A post that is an advert with a keyword on top fails review.
- **Written to the manual's prose rules** (section 12): plain words, active voice, one idea per sentence, no stock machine phrasing, no keyword stuffing.
- **As long as the question needs, and never thin.** A full post runs at least 1,500 words and a short one at least 800, all of them earned. Reach the length by covering more of the question: the follow-up questions from the section plan, a worked example, the edge cases, the common mistakes, how to choose between options. Never by restating, long introductions or filler. If a keyword cannot clear its floor with useful words, it is too narrow for its own post: make it a secondary keyword of a broader post and say so.
- **Sectioned for the sidebar.** 4 to 8 second-level headings, each one naming what its section answers, and at least 2 of them written as the question a reader would ask. They become the table of contents, so a reader should be able to follow the post from the headings alone.

### The reviewer brief

docs/blog-plan/REVIEWER-BRIEF.md, to fill in:

```markdown
# Reviewer brief

You review the finished posts of one category of the blog at <site>. You
did not write them. You read each post against the list below, fix what
fails, and report. You may edit the post files in your category. You never
change a slug, title, primary keyword, category or second-level heading,
never edit another category's files, never edit code, scripts or the
keyword map, and never write to the database.

## Read first

- docs/blog-plan/WRITER-BRIEF.md: the rules the writers worked to, and
  the product facts.
- docs/blog-keywords.md: each post's block, with the question it answers
  and the question each heading answers.

## For each post

1. Untraceable claims. Open every cited source and confirm it says what
   the post says. A claim you cannot trace to the repository, a linked
   source or common knowledge is removed, not softened. A source that
   does not load goes, with the claim it supported.
2. Opening paragraphs. Read the first paragraph alone against the
   "Answers:" question, and each section opener against its heading. One
   that does not answer, or needs the rest of the post, is rewritten
   within the word bounds.
3. Padding. A section that repeats another or could be deleted without
   loss is replaced with substance, and the post still clears its floor.
4. Product truth. Any sentence that says the project does something its
   source files do not describe is corrected. Advert paragraphs are cut
   to one or two plain sentences.
5. Prose. Remove hype, stock machine phrasing and filler. Keep the
   writer's voice otherwise. Do not rewrite for taste.
6. Overlap within the category. Read the posts side by side. Where two
   sections explain the same subtopic, keep the fuller one in the owner
   and cut the other to a two-sentence summary with a link to the owner's
   anchor. The trimmed post still clears its floor.

## Verify

<the content check command> --files --slug <all your slugs>

It must pass. Do not change any rule or script.

## Report

Under 250 words: Act on (what you changed, in which post), Consider,
Noted, Dismissed (with a reason), then the claims removed and the sources
that failed to load. End with PASS or ISSUES.
```

### Review before they ship

- The judge is never the author. A reviewer that did not write the post reads each one against the brief.
- It flags every claim it cannot trace to the repository, to a linked source or to common knowledge. An untraceable claim is removed, not softened. It opens each cited source and confirms the source says what the post says it does.
- It reads each opening paragraph alone, against the question in the section plan. A paragraph that does not answer the question, or that needs the rest of the post to make sense, is rewritten.
- It checks the category as a whole for two posts answering the same question, and for a post filed under a category it does not fit.
- It reads the posts side by side for sections that cover the same subtopic, including in different words. The fuller section stays in the post that owns the subtopic. The other becomes a short summary with a link, and that post still clears the length floor afterwards with material of its own.
- It flags padding: a section that repeats another, or that could be deleted without the reader losing anything. Padding is replaced with substance, and the post still clears the length floor afterwards.
- Sort findings into `Act on`, `Consider`, `Noted` and `Dismissed`, with a reason for each dismissal. The lead reads `Consider` and decides.

## Step 8. Prove it on the real surface

Manual section 5. "It builds" is not evidence. Run the real thing and paste what happened. A check that could not run is reported as inconclusive, not skipped.

| Claim | Proof |
| --- | --- |
| The pages work | Open `/blog`, `/blog/recent`, `/blog/recent/updated`, one category page, one post, an unknown category and an unknown slug in a real browser. Paste each URL, its status and what rendered. |
| The plan is complete | The count of keyword-map rows in each status, and the published count per category beside its planned count, from the check's own report. Every row is published. |
| The recent list is right | The entries of `/blog/recent` in order, beside the loader's newest 100 published posts. They match, and no draft is among them. |
| The updated list is right | The entries of `/blog/recent/updated`, beside the loader's posts that have an updated date, in that order. No post without one appears. |
| The categories work | Click every card on the index and paste where each landed. Each category page lists only its own posts, and the counts on the cards match the pages. |
| Categories are balanced | The post count per category, from the content check's own output. |
| It fits the site | Screenshots of the index, both lists, a category page and a post at desktop and phone width, with no horizontal overflow and no console errors. |
| The table of contents and the call to action work | On one post at desktop width, click an entry and paste where the page landed, and show the call to action box under the entries. At phone width, show where the table of contents and its call to action sit. Confirm the entries match the post's headings and the call to action appears again at the end of the body, followed by the related posts. |
| Posts are long enough | The word count of every post beside its floor, from the content check's own output. |
| Posts answer first | The opening paragraph of every post with its word count, from the content check's own output, beside the question it answers. |
| Answer engines can read it | Fetch one post with a plain HTTP client that runs no scripts and paste a sentence from its last section out of the response. Fetch its Markdown copy and the blog's `llms.txt`, and paste the first lines of each. |
| Crawlers are not blocked | The served `robots.txt`, and for GPTBot, OAI-SearchBot, ClaudeBot, PerplexityBot and Google-Extended, whether each may fetch a post. |
| Posts do not repeat each other | The highest section overlap for every pair of posts, from the content check's own output, and the reviewers' lists of sections they replaced with a summary and a link. |
| Posts are stored where the brief says | With a database: the migration output, then a query of the `posts` table showing the count per category and draft flag, matching the index. Then edit one post through the write function and show the page change and the updated list change without a rebuild. Without one: the list of post files. |
| Metadata is right | Read the title, description, canonical URL, social tags and preview image from the served HTML of one post. Parse its structured data and paste the `BlogPosting` author, both dates and the `BreadcrumbList`. |
| Search engines can find it | Fetch the sitemap and the feed from the running app. Every post is in both, both lists and every category page are in the sitemap with a last-modified date, the feed parses, and the page head links to the feed. |
| No post is an orphan | For every post, the posts that link to it, from the content check's own output. |
| The check works | The verify command output over the whole blog, plus the test run showing each rule rejects its failing fixture. |
| Navigation works | Click from the homepage to the blog, from the index to each list and on to a post, from a category card to its page, and from there to a post and back to its category. |

Write the browser part as a script the project keeps, so the next agent can run it after adding a post. If the project has a verification skill that drives the app, use it. If it has none and the project has a web surface, say that it is missing. Building one is manual section 5 and is worth proposing as follow-up work.

## Step 9. Leave it extendable, then ship

Manual sections 6, 10 and 11. The next post should be a lookup and a skill run, not a rediscovery.

- **`docs/blog.md`.** A living document: the routes, where posts live (the table or the directory, and why), where the category, author and call-to-action records live, the post, category and author shapes, the write function and the seed command, how to add a post or a category, what the content check enforces, and how to run it and the browser proof.
- **A skill for adding a post.** Save it in the project's skills directory, adapted to the real paths. Run it once by hand to add the last post of this run, so you know it works.
- **How to keep posts fresh.** In `docs/blog.md`, say when a post is due a second look (a product change it describes, or a year since it was last checked) and that `updatedAt` moves only when the substance does, because the recently updated list, the feed and the sitemap all show it.
- **What to measure.** In `docs/blog.md`, list what the user can watch once the blog is live: search-console impressions and queries per post, visits referred by AI assistants, and whether an assistant asked one of the section-plan questions cites the post. You cannot measure any of these from the repository, so report none of them.
- **The feature map.** If the project keeps one, add rows for the blog index, both lists, the category page, the post page, the Markdown copies, the `llms.txt`, the sitemap and the feed. If it does not, list those surfaces in `docs/blog.md`.
- **The agent instruction file.** Add one line pointing at `docs/blog.md`. Do not paste the rules into it. The check enforces them.
- **A decision log** (manual section 10): keywords chosen, categories chosen, generator chosen, anything grouped, de-duplicated, skipped or reverted, with the time and the evidence. A run this size is always long.

the add-a-post skill, to adapt:

```markdown
---
name: add-blog-post
description: Add one post to the blog. Use when asked to write, add or
  publish a blog post, or to target a new keyword.
---

1. Read docs/blog.md, the keyword map and the section plan. If the
   keyword already has a post, improve that post instead of writing a
   second one.
2. Pick the category. Count the posts in each one first. Where the topic
   fits more than one, choose the category with the fewest posts. Add a
   new category only when 2 or more posts will live in it.
3. Add the keyword-map row first: keyword, intent, length, category,
   title, slug, status planned. Posts are served at /blog/<slug>, so
   the slugs recent, category, feed.xml, llms.txt and every other segment a fixed
   route under /blog uses are taken.
4. Add the post's block to the section plan: the question the post
   answers, then one heading per follow-up question, the concrete
   element, and the posts it links to. A subtopic another post already
   owns gets one or two sentences and a link to that section, not a
   section of its own. No heading repeats one anywhere in the blog.
5. Write the post from the writer brief in docs/blog-plan/WRITER-BRIEF.md
   and the pattern post it names. Every claim about the project comes
   from the repository, and every claim from outside it links to its
   source. No invented numbers, quotes or customers. The body opens with
   a 40 to 60 word answer, then runs at least 1,500 words (800 for a
   short post) under 4 to 8 second-level headings, 2 of them questions.
6. Link to two other posts, one of them in the same category, and to
   one page of the project. Add a link to the new post from an existing
   post that mentions its subject, so it is not an orphan. If that
   post's substance changed, set its updatedAt.
7. Run the content check on the new post and the post you linked from.
   With a database, save both through the one write function (it
   validates, then upserts by slug) and revalidate the pages. Then run
   the verify command. The content check must pass, including the
   category balance rule and the overlap rules.
8. Open the post in the running app, click an entry in its sidebar table
   of contents, confirm the call to action shows under the table of
   contents and at the end of the post, and paste what you saw. Open its
   category page and the recent list and confirm the post is listed on
   both.
9. Set the row's status to published in the keyword map.

When the task is to improve an existing post instead: change it, set
updatedAt only if the substance changed, confirm it now leads the
recently updated list, and run steps 7 and 8.
```

- Commit in the order the units were built, so the history proves itself: model, storage and loader, index and category pages, post page, both lists, search surface, answer-engine surface, check with its fixtures, the plan, then the posts one category per commit. The migration goes in the same commit as the table it creates.
- Follow the project's rules on committing, pushing and pull requests. Where they are silent, open one pull request whose description has Why, Scope, Blast radius and Verification (manual section 11).
- Do not deploy, do not submit the sitemap to any search console, and do not notify any indexing service. Those are the user's to do.

## The reply

End with one reply in this order. Short declarative sentences, and every claim labeled measured, inferred or guess.

1. What exists now and for whom: the blog URL, the recent and recently updated list URLs, the number of posts and categories, where a reader finds them.
2. The categories as a table, each with its URL, its planned count and its published count, saying which were given and which you derived.
3. The keyword map's totals by status, and where the full map is. Not the whole map.
4. The defaults you applied for inputs the user did not give, each with the one word that reverses it.
5. Where posts are stored, database or files, and what you found that decided it.
6. The author you credited and where the name came from, the call to action you used and where its words came from, and the crawler policy you found in `robots.txt`.
7. The finish condition, item by item, each with its proof or marked inconclusive.
8. The manual rules that changed a decision, and the decision.
9. What needs the user: deploying, submitting the sitemap to search consoles, deciding which AI crawlers to allow, any claim a post could not make without facts you did not have.
10. **Attention.** What deserves the user's scrutiny first. "No flags" is a valid value.
