/blog — Blog post generator
Creates a production-ready blog post end-to-end for any site managed in sites/.
Inputs
Required (asked at start if not given):
- Site — must match a folder under
sites/[name]/. The skill reads its site-info.md.
Optional (per-run overrides):
- Primary keyword — if not given, picks the highest-priority unused keyword from
keywords.csv.
- Image source — overrides the site's default. Options: Pexels, Unsplash, site-library, AI-generated, client-supplied, none.
- Keyword tool — overrides the site's default for any keyword research the skill performs.
- Length target — bypasses the SERP-median-derived target (rare; only when the user has a specific reason).
Workflow
Step 0 — Cron-mode detection (if invoked with --cron)
If the invocation contains --cron, this skill runs in cron mode (no user available). Required behavior:
Idempotency check: see this skill's "Routine versioning + idempotency contract" section. If today's output already exists, exit cleanly with exit: "idempotent-skip". Write one line to the audit log via scripts/lib/audit-log.mjs appendRun({ exit: "idempotent-skip", ... }).
Escalation contract: any decision that would normally prompt the user (missing required file, voice anti-AI failure, schema validation failure, keyword cannibalization, etc.) MUST be escalated by writing an item to sites/{site}/_inbox/ via scripts/lib/cron-mode.mjs writeInboxItem(...). After writing, exit cleanly with exit: "escalated". Do NOT use AskUserQuestion in cron mode.
Defaults: when a choice would normally be asked, default to site-info.md / goals.md values. If both are silent on the required choice, escalate per (2).
Audit log: ALWAYS write one line to the audit log on exit — success (shipped), escalation (escalated), idempotent skip (idempotent-skip), or failure (failed).
Backoff: at the start of every cron-mode run, call checkBackoff({ routine }) from scripts/lib/audit-log.mjs. If true, the routine has hit the 3-strike threshold — write _inbox/routine-disabled-{name}.md, run scripts/sync-schedules.mjs --pause-routine={name}, and exit.
Step 1 — Read context (Rule 1)
Load in order:
CLAUDE.md (toolkit root)
SEO_GUIDE.md
on-page-seo.md
sites/[site]/site-info.md
- Resolved
references/ — root files, then per-site overrides if present
sites/[site]/keywords.csv and sites/[site]/used-keywords.md
sites/[site]/notes.md
If any required file is missing, ask the user before proceeding.
Step 2 — Pick the keyword
- If user supplied a primary keyword → use it.
- Otherwise: read
keywords.csv, exclude any keyword listed in used-keywords.md, sort by opportunity score (high volume × low difficulty × intent match), pick top.
- Seasonality check (soft) —
SEO_GUIDE.md Section 2.5. If the picked keyword has peak_months and/or seasonality populated:
- If today is in
peak_months OR seasonality = stable → proceed.
- If
seasonality = seasonal or holiday-spike AND today is NOT in peak_months → flag and ask:
"This keyword peaks in [months]. Today is [month]. Options: (a) proceed — early publish gives Google time to crawl + rank before peak; (b) pick a year-round keyword from keywords.csv; (c) defer to ~3 months before peak. Which?"
- If
seasonality = declining → flag and ask before continuing.
- Never refuse on seasonality alone — option (a) is always available.
- Confirm choice with the user before proceeding.
Step 3 — Build the cluster (SEO_GUIDE.md Section 3.3 + Section 2.4)
- 1 primary keyword + 4–8 supporting (2–3 secondary, 3–5 tertiary).
- Pull supporting keywords from the keyword tool (per
site-info.md default or per-run override).
- Validate every cluster term against the primary using the SERP-comparison test (
SEO_GUIDE.md Section 2.4):
- Search both keywords incognito (target region per
site-info.md).
- Count shared URLs in the top 10.
- ≥ 4 shared → cluster term belongs on this page. Keep.
- 2–3 shared → borderline. Keep only if the cluster term has < 100/mo volume (tertiary).
- ≤ 1 shared → drop. The term belongs on a different page; flag for the user as a candidate future post.
- Final cluster: only terms that pass the SERP-comparison test. Reject candidates that look semantically similar but fail the test — the rule is one-page-one-intent (
CLAUDE.md).
Step 4 — SERP analysis (SEO_GUIDE.md Section 3.5)
- Search Google (incognito, target region per
site-info.md) for the primary keyword.
- Identify top 3 organic results, skipping: Reddit, Quora, forums, YouTube, Wikipedia, paywalled, brand homepages, the site itself.
- Extract from each: word count, H2/H3 outline, image count, FAQ questions, internal/external link patterns.
- Calculate median word count + image count → target ±20%.
- Identify 1–2 novel sections the top 3 missed.
- Check Perplexity + Google AI Overviews citations for the keyword — note formats (lists, tables, direct answers) AI rewards.
Step 4.5 — Wireframe (mandatory, per CLAUDE.md "Wireframe before content" rule)
Before generating prose, produce a wireframe doc at sites/[site]/_drafts/[slug]/wireframe.md. Mandatory contents:
- Layout zones (top-to-bottom): hero (H1 + intro), TOC if applicable, content sections (one per H2), FAQ, related-posts/services, author bio.
- Heading map (H1 / H2 / H3 with placeholder text matching the cluster from Step 3).
- Internal linking pattern slots — which of the 7 patterns from
SEO_GUIDE.md Section 5.1 the page carries. For a blog post: patterns 1 (header dropdown via template), 3 (footer via template), 4 (breadcrumb), and 7 (blog → transactional bridge — explicitly call out which service/product page the post links to).
- Image plan — hero image + N inline images, per
/blog Step 7 image source.
- External link plan — 2–3 authoritative sources by topic (.gov, .edu, major industry).
- CTA placement — typical for blog: 1 mid-post CTA + 1 end-post CTA, both pointing to the linked transactional page.
Optional (if user opts in): use draw.io or hand-sketch → photo, then have AI produce a first visual mockup to share with the client. Toolkit doesn't generate the visual mockup — the wireframe doc is enough for skill purposes; visuals are a presentation-time enhancement.
Show the wireframe to the user. Wait for explicit approval before generating prose. Approved wireframes get archived in _drafts/[slug]/; rejected ones get revised and re-shown.
Step 5 — Plan + approval (Rule 2)
Present to user:
- Primary keyword + cluster
- Target word count + image count
- Proposed H1, H2 outline, FAQ questions
- Novel sections to add
- Image source for this run
- Language fan-out (multilingual sites only) — list of languages this draft will be produced in (default: all declared in
site-info.md).
Wait for approval before generating.
Step 5.5 — Resolve language fan-out (multilingual sites only)
Read site-info.md Languages section.
Multilingual: false → skip this step; continue to Step 6 as a single-language run.
Multilingual: true:
- Default coverage = every language declared in the Languages table.
- If user supplied
languages: [...] opt-out (a subset) → confirm explicitly before honoring. Default refuses to ship a unilingual draft on a multilingual site.
- For each declared language:
- Resolve voice files: root
references/voice.md + per-site references/voice.[lang].md (REQUIRED — refuse if missing) + per-site references/humour.[lang].md / stories.[lang].md / opinions.[lang].md (optional). stats.md is shared.
- Format URL using the language's URL pattern from the Languages table (substitute
[slug]).
- Reserve a draft folder:
sites/[site]/_drafts/[slug]/[lang]/.
- Generate the hreflang link cluster from the resolved language set (self-reference + all siblings +
x-default).
- Schema
inLanguage is set per-language draft.
The skill produces N drafts (one per language), each in its own _drafts/[slug]/[lang]/ subfolder. Each draft passes its own Tier 1 + voice anti-AI check using its language's voice files.
Step 6 — Generate the post
Apply, in this order:
- Voice — resolved
references/ files (root + per-site override). Anti-AI rules from CLAUDE.md.
- Tier 1 on-page — all 16 items.
- Cluster placement — primary in title/H1/first 100 words/slug; secondary in H2s; tertiary in H3s, FAQ, body.
- One story max (from
stories.md), one strong opinion max (from opinions.md, backed by a number from stats.md).
- At least one "when NOT to use / hire us" moment.
- 3–5 internal links (to other posts/services on this same site, using
used-keywords.md and the site's known URLs as candidates).
- 2–3 external links to authoritative sources.
- FAQ section — 4–8 Q+A pulled from the keyword tool's Questions view + Google's PAA.
- Author byline per the site's
site-info.md → "Public byline policy" + (if the site has one) sites/{site}/coi-categories.md. Per-post author: frontmatter is REQUIRED and routed:
author: your-name-slug (or any real-person slug) → load sites/{site}/author-{slug}.md, inject the bio block in the page's author footer, inject Person schema in @graph. Refuse to ship if any {TO FILL} placeholder remains in author-{slug}.md.
author: contributor → same as above, with the active contributor's slug per site-info.
author: brand → reference the site's Organization @id as author. NO Person schema. Bio footer references the org's About page.
- For sites where
site-info.md declares a single byline mode (e.g., site-b: always co-byline; site-c: always real-founder), the per-post author: field is auto-set from that policy and the routing above is bypassed.
- Auto-determination (only for sites with
coi-categories.md): map post's primary keyword → category via keywords.csv / service-keywords.csv, look up category in coi-categories.md. If category appears in personal_eligible → author: your-name-slug. If category appears in coi_adjacent AND a contributor is onboarded → author: contributor. If category appears in coi_adjacent AND no contributor → author: brand (interim default). If category in NEITHER list → escalate to user (refuse to auto-classify).
- Schema (JSON-LD) —
BlogPosting (or appropriate subtype) + BreadcrumbList + FAQPage. Plus Person IFF author: your-name-slug | contributor (a real person), OR Organization-as-author IFF author: brand.
- Length within ±20% of SERP median.
- TOC + jump links if word count ≥ 1500 (
on-page-seo.md Category 15).
- AI-search-friendly: Q+A density. Beyond the dedicated FAQ section (item 8), structure 5+ in-body sections as explicit question→answer pairs (the H2 or H3 IS a question; the paragraph below IS the answer in the first sentence). LLMs preferentially cite passages that stand alone as direct answers. Examples:
## What does an AI consultant actually do? (not ## The role of AI consultants); ### Should you hire an AI consultant before reaching $500K revenue? (not ### Timing considerations).
- AI-search-friendly: citation-friendly chunking. Paragraphs max 3 sentences. Lead each paragraph with the topic sentence — the main claim or answer. Front-load named entities (brands, places, products, people) in the sentence rather than burying them in subordinate clauses. LLMs preferentially cite paragraphs that read as standalone units.
- AI-search-friendly: self-contained facts. Every paragraph must stand on its own. NO "as mentioned above," "as discussed earlier," "see the previous section," "we'll cover this later." LLMs lose context between paragraphs — write as if each paragraph is the only one cited. Restate key entities and context within the paragraph rather than referencing prior copy.
- AI-search-friendly: verifiable claims. Every statistic, percentage, or numeric claim cites a source via inline link — McKinsey, Zapier State of Business Automation, Google's own data, BLS, public industry reports. Unsourced numbers read as fabricated to both readers and LLM citation engines. If you can't source a number, either drop it or label it as "Site A estimate" (only fine when explicit and rare).
Step 7 — Fetch images
Per the chosen image source:
- Pexels / Unsplash —
node scripts/fetch-images.mjs --source=[name] --query="[primary keyword]" --count=[n] --site=[site] --slug=[slug] --hero
- Site library — present a list from the site's media library; user picks.
- AI-generated — pause and ask the user to drop generated images into
_drafts/[slug]/images/.
- Client-supplied — pause and wait for upload to
_drafts/[slug]/images/.
- None — skip.
For all sources: enforce WebP under 200 KB, hyphenated filenames, descriptive alt text, width/height attributes, lazy loading on below-fold, eager + fetchpriority on hero.
Step 8 — Validate (Rule 4)
Run all validation checks:
- ✅ Tier 1 (16 items) — auto-checked
- ✅ Schema validators —
node scripts/validate-schema.mjs --url=[staged URL]
- ✅ Voice anti-AI check — re-read resolved
voice.md "Tells that it's AI-written"; delete matches
- ✅ Cadence — verify publishing this post wouldn't exceed
site-info.md → Content cadence
- ✅ Cannibalization — primary keyword not in
used-keywords.md
- ✅ Banned-words scan
- ✅ Lighthouse spot-check on staged URL —
node scripts/lighthouse.mjs --url=[staged URL]
If any check fails: refuse to ship, report what needs fixing, ask user how to proceed.
Step 9 — Ship per publishing method
Read site-info.md → Publishing method:
repo-commit — write markdown/MDX file to the site's repo path, commit images to public/blog/[slug]/, open a PR (or commit directly to a branch named blog/[slug]). Output: PR URL.
cms-paste — write to sites/[site]/_drafts/[slug]/post.md + meta.json (with title, meta description, OG tags, schema, image filenames). Output: clipboard-ready content + image folder path.
lovable-prompt — generate a Lovable-ready prompt embedding the post content and structural instructions. Output: paste-ready prompt.
headless-api — push via the configured API (Sanity / Contentful / Strapi). Output: CMS draft URL.
Step 10 — Update tracker
Append to sites/[site]/used-keywords.md:
| YYYY-MM-DD | [primary keyword] | Blog | [URL] | [cluster keywords comma-separated] |
Step 11 — Report
Print a summary:
- Post title + URL
- Word count + image count
- Tier 1 status (all 16 ✅)
- Schema validation status
- Lighthouse score (if checked)
- Cluster keywords used
- Next recommended action (e.g., "Submit to GSC for indexing within 7 days")
Routine versioning + idempotency contract
This skill participates in cruise-control via --cron mode. When invoked by cron:
Stamp routine_version in every output produced (the notes.md audit entry header, _inbox/ item frontmatter, draft folder metadata, audit log line written via scripts/lib/audit-log.mjs). Current routine_version: 1.0. Bump when the skill's behavior meaningfully changes.
Idempotency: this skill MUST be safe to run twice in a row on the same day without producing duplicate work. Implementation: check _drafts/{YYYY-MM-DD}-* at the start of every cron-mode run; if today's slug already exists, exit cleanly with exit: "idempotent-skip".
Reference: docs/specs/2026-05-16-agents-cruise-control-design.md § Operational hardening O3.
Refusal conditions
The skill refuses to run if:
- Site folder doesn't exist or
site-info.md missing.
- Resolved
references/ is incomplete (root files missing).
keywords.csv is empty (and user didn't supply a primary keyword).
- Publishing this post would exceed cadence.
- Primary keyword already in
used-keywords.md for this site.
1---2name: blog3description: Generate a long-form, SEO-optimized, voice-matched blog post for a chosen managed site. Picks an unused informational keyword, runs SERP analysis on the top 3 results, fetches images per the site's configured source, applies the resolved voice (root + per-site override), satisfies Tier 1 on-page SEO, validates schema, and ships in the format the site's publishing method requires (repo-commit / cms-paste / lovable-prompt / headless-api). Use when the user types `/blog` or asks for a new blog post.4---56# `/blog` — Blog post generator78Creates a production-ready blog post end-to-end for any site managed in `sites/`.910## Inputs1112Required (asked at start if not given):13- **Site** — must match a folder under `sites/[name]/`. The skill reads its `site-info.md`.1415Optional (per-run overrides):16- **Primary keyword** — if not given, picks the highest-priority unused keyword from `keywords.csv`.17- **Image source** — overrides the site's default. Options: Pexels, Unsplash, site-library, AI-generated, client-supplied, none.18- **Keyword tool** — overrides the site's default for any keyword research the skill performs.19- **Length target** — bypasses the SERP-median-derived target (rare; only when the user has a specific reason).2021## Workflow2223### Step 0 — Cron-mode detection (if invoked with `--cron`)2425If the invocation contains `--cron`, this skill runs in cron mode (no user available). Required behavior:26271. **Idempotency check:** see this skill's "Routine versioning + idempotency contract" section. If today's output already exists, exit cleanly with `exit: "idempotent-skip"`. Write one line to the audit log via `scripts/lib/audit-log.mjs` `appendRun({ exit: "idempotent-skip", ... })`.28292. **Escalation contract:** any decision that would normally prompt the user (missing required file, voice anti-AI failure, schema validation failure, keyword cannibalization, etc.) MUST be escalated by writing an item to `sites/{site}/_inbox/` via `scripts/lib/cron-mode.mjs` `writeInboxItem(...)`. After writing, exit cleanly with `exit: "escalated"`. Do NOT use `AskUserQuestion` in cron mode.30313. **Defaults:** when a choice would normally be asked, default to `site-info.md` / `goals.md` values. If both are silent on the required choice, escalate per (2).32334. **Audit log:** ALWAYS write one line to the audit log on exit — success (`shipped`), escalation (`escalated`), idempotent skip (`idempotent-skip`), or failure (`failed`).34355. **Backoff:** at the start of every cron-mode run, call `checkBackoff({ routine })` from `scripts/lib/audit-log.mjs`. If true, the routine has hit the 3-strike threshold — write `_inbox/routine-disabled-{name}.md`, run `scripts/sync-schedules.mjs --pause-routine={name}`, and exit.3637### Step 1 — Read context (Rule 1)3839Load in order:401. `CLAUDE.md` (toolkit root)412. `SEO_GUIDE.md`423. `on-page-seo.md`434. `sites/[site]/site-info.md`445. Resolved `references/` — root files, then per-site overrides if present456. `sites/[site]/keywords.csv` and `sites/[site]/used-keywords.md`467. `sites/[site]/notes.md`4748If any required file is missing, ask the user before proceeding.4950### Step 2 — Pick the keyword5152- If user supplied a primary keyword → use it.53- Otherwise: read `keywords.csv`, exclude any keyword listed in `used-keywords.md`, sort by opportunity score (high volume × low difficulty × intent match), pick top.54- **Seasonality check (soft) — `SEO_GUIDE.md` Section 2.5.** If the picked keyword has `peak_months` and/or `seasonality` populated:55 - If today is in `peak_months` OR `seasonality = stable` → proceed.56 - If `seasonality = seasonal` or `holiday-spike` AND today is NOT in `peak_months` → flag and ask:57 > "This keyword peaks in [months]. Today is [month]. Options: (a) proceed — early publish gives Google time to crawl + rank before peak; (b) pick a year-round keyword from `keywords.csv`; (c) defer to ~3 months before peak. Which?"58 - If `seasonality = declining` → flag and ask before continuing.59 - Never refuse on seasonality alone — option (a) is always available.60- Confirm choice with the user before proceeding.6162### Step 3 — Build the cluster (`SEO_GUIDE.md` Section 3.3 + Section 2.4)6364- 1 primary keyword + 4–8 supporting (2–3 secondary, 3–5 tertiary).65- Pull supporting keywords from the keyword tool (per `site-info.md` default or per-run override).66- **Validate every cluster term against the primary using the SERP-comparison test (`SEO_GUIDE.md` Section 2.4):**67 - Search both keywords incognito (target region per `site-info.md`).68 - Count shared URLs in the top 10.69 - **≥ 4 shared** → cluster term belongs on this page. Keep.70 - **2–3 shared** → borderline. Keep only if the cluster term has < 100/mo volume (tertiary).71 - **≤ 1 shared** → drop. The term belongs on a different page; flag for the user as a candidate future post.72- Final cluster: only terms that pass the SERP-comparison test. Reject candidates that look semantically similar but fail the test — the rule is one-page-one-intent (`CLAUDE.md`).7374### Step 4 — SERP analysis (`SEO_GUIDE.md` Section 3.5)75761. Search Google (incognito, target region per `site-info.md`) for the primary keyword.772. Identify top 3 organic results, skipping: Reddit, Quora, forums, YouTube, Wikipedia, paywalled, brand homepages, the site itself.783. Extract from each: word count, H2/H3 outline, image count, FAQ questions, internal/external link patterns.794. Calculate median word count + image count → target ±20%.805. Identify 1–2 novel sections the top 3 missed.816. Check Perplexity + Google AI Overviews citations for the keyword — note formats (lists, tables, direct answers) AI rewards.8283### Step 4.5 — Wireframe (mandatory, per `CLAUDE.md` "Wireframe before content" rule)8485Before generating prose, produce a wireframe doc at `sites/[site]/_drafts/[slug]/wireframe.md`. Mandatory contents:8687- **Layout zones** (top-to-bottom): hero (H1 + intro), TOC if applicable, content sections (one per H2), FAQ, related-posts/services, author bio.88- **Heading map** (H1 / H2 / H3 with placeholder text matching the cluster from Step 3).89- **Internal linking pattern slots** — which of the 7 patterns from `SEO_GUIDE.md` Section 5.1 the page carries. For a blog post: patterns 1 (header dropdown via template), 3 (footer via template), 4 (breadcrumb), and **7 (blog → transactional bridge — explicitly call out which service/product page the post links to)**.90- **Image plan** — hero image + N inline images, per `/blog` Step 7 image source.91- **External link plan** — 2–3 authoritative sources by topic (.gov, .edu, major industry).92- **CTA placement** — typical for blog: 1 mid-post CTA + 1 end-post CTA, both pointing to the linked transactional page.9394Optional (if user opts in): use draw.io or hand-sketch → photo, then have AI produce a first visual mockup to share with the client. Toolkit doesn't generate the visual mockup — the wireframe doc is enough for skill purposes; visuals are a presentation-time enhancement.9596Show the wireframe to the user. Wait for explicit approval before generating prose. Approved wireframes get archived in `_drafts/[slug]/`; rejected ones get revised and re-shown.9798### Step 5 — Plan + approval (Rule 2)99100Present to user:101- Primary keyword + cluster102- Target word count + image count103- Proposed H1, H2 outline, FAQ questions104- Novel sections to add105- Image source for this run106- **Language fan-out** *(multilingual sites only)* — list of languages this draft will be produced in (default: all declared in `site-info.md`).107108Wait for approval before generating.109110### Step 5.5 — Resolve language fan-out (multilingual sites only)111112Read `site-info.md` Languages section.113114- **`Multilingual: false`** → skip this step; continue to Step 6 as a single-language run.115- **`Multilingual: true`**:116 1. Default coverage = every language declared in the Languages table.117 2. If user supplied `languages: [...]` opt-out (a subset) → confirm explicitly before honoring. Default refuses to ship a unilingual draft on a multilingual site.118 3. For each declared language:119 - Resolve voice files: root `references/voice.md` + per-site `references/voice.[lang].md` (REQUIRED — refuse if missing) + per-site `references/humour.[lang].md` / `stories.[lang].md` / `opinions.[lang].md` (optional). `stats.md` is shared.120 - Format URL using the language's URL pattern from the Languages table (substitute `[slug]`).121 - Reserve a draft folder: `sites/[site]/_drafts/[slug]/[lang]/`.122 4. Generate the hreflang link cluster from the resolved language set (self-reference + all siblings + `x-default`).123 5. Schema `inLanguage` is set per-language draft.124125The skill produces N drafts (one per language), each in its own `_drafts/[slug]/[lang]/` subfolder. Each draft passes its own Tier 1 + voice anti-AI check using its language's voice files.126127### Step 6 — Generate the post128129Apply, in this order:1301. **Voice** — resolved `references/` files (root + per-site override). Anti-AI rules from `CLAUDE.md`.1312. **Tier 1 on-page** — all 16 items.1323. **Cluster placement** — primary in title/H1/first 100 words/slug; secondary in H2s; tertiary in H3s, FAQ, body.1334. **One story max** (from `stories.md`), **one strong opinion max** (from `opinions.md`, backed by a number from `stats.md`).1345. **At least one "when NOT to use / hire us"** moment.1356. **3–5 internal links** (to other posts/services on this same site, using `used-keywords.md` and the site's known URLs as candidates).1367. **2–3 external links** to authoritative sources.1378. **FAQ section** — 4–8 Q+A pulled from the keyword tool's Questions view + Google's PAA.1389. **Author byline** per the site's `site-info.md` → "Public byline policy" + (if the site has one) `sites/{site}/coi-categories.md`. Per-post `author:` frontmatter is REQUIRED and routed:139 - **`author: your-name-slug`** (or any real-person slug) → load `sites/{site}/author-{slug}.md`, inject the bio block in the page's author footer, inject `Person` schema in `@graph`. Refuse to ship if any `{TO FILL}` placeholder remains in `author-{slug}.md`.140 - **`author: contributor`** → same as above, with the active contributor's slug per site-info.141 - **`author: brand`** → reference the site's `Organization` `@id` as author. NO `Person` schema. Bio footer references the org's About page.142 - For sites where `site-info.md` declares a single byline mode (e.g., site-b: always co-byline; site-c: always real-founder), the per-post `author:` field is auto-set from that policy and the routing above is bypassed.143 - **Auto-determination** (only for sites with `coi-categories.md`): map post's primary keyword → category via `keywords.csv` / `service-keywords.csv`, look up category in `coi-categories.md`. If category appears in `personal_eligible` → `author: your-name-slug`. If category appears in `coi_adjacent` AND a contributor is onboarded → `author: contributor`. If category appears in `coi_adjacent` AND no contributor → `author: brand` (interim default). If category in NEITHER list → escalate to user (refuse to auto-classify).14410. **Schema (JSON-LD)** — `BlogPosting` (or appropriate subtype) + `BreadcrumbList` + `FAQPage`. Plus `Person` IFF `author: your-name-slug | contributor` (a real person), OR Organization-as-author IFF `author: brand`.14511. **Length** within ±20% of SERP median.14612. **TOC + jump links** if word count ≥ 1500 (`on-page-seo.md` Category 15).14713. **AI-search-friendly: Q+A density.** Beyond the dedicated FAQ section (item 8), structure 5+ in-body sections as explicit question→answer pairs (the H2 or H3 IS a question; the paragraph below IS the answer in the first sentence). LLMs preferentially cite passages that stand alone as direct answers. Examples: `## What does an AI consultant actually do?` (not `## The role of AI consultants`); `### Should you hire an AI consultant before reaching $500K revenue?` (not `### Timing considerations`).14814. **AI-search-friendly: citation-friendly chunking.** Paragraphs max 3 sentences. Lead each paragraph with the topic sentence — the main claim or answer. Front-load named entities (brands, places, products, people) in the sentence rather than burying them in subordinate clauses. LLMs preferentially cite paragraphs that read as standalone units.14915. **AI-search-friendly: self-contained facts.** Every paragraph must stand on its own. NO "as mentioned above," "as discussed earlier," "see the previous section," "we'll cover this later." LLMs lose context between paragraphs — write as if each paragraph is the only one cited. Restate key entities and context within the paragraph rather than referencing prior copy.15016. **AI-search-friendly: verifiable claims.** Every statistic, percentage, or numeric claim cites a source via inline link — McKinsey, Zapier State of Business Automation, Google's own data, BLS, public industry reports. Unsourced numbers read as fabricated to both readers and LLM citation engines. If you can't source a number, either drop it or label it as "Site A estimate" (only fine when explicit and rare).151152### Step 7 — Fetch images153154Per the chosen image source:155- **Pexels / Unsplash** — `node scripts/fetch-images.mjs --source=[name] --query="[primary keyword]" --count=[n] --site=[site] --slug=[slug] --hero`156- **Site library** — present a list from the site's media library; user picks.157- **AI-generated** — pause and ask the user to drop generated images into `_drafts/[slug]/images/`.158- **Client-supplied** — pause and wait for upload to `_drafts/[slug]/images/`.159- **None** — skip.160161For all sources: enforce WebP under 200 KB, hyphenated filenames, descriptive alt text, width/height attributes, lazy loading on below-fold, eager + fetchpriority on hero.162163### Step 8 — Validate (Rule 4)164165Run all validation checks:166- ✅ Tier 1 (16 items) — auto-checked167- ✅ Schema validators — `node scripts/validate-schema.mjs --url=[staged URL]`168- ✅ Voice anti-AI check — re-read resolved `voice.md` "Tells that it's AI-written"; delete matches169- ✅ Cadence — verify publishing this post wouldn't exceed `site-info.md` → Content cadence170- ✅ Cannibalization — primary keyword not in `used-keywords.md`171- ✅ Banned-words scan172- ✅ Lighthouse spot-check on staged URL — `node scripts/lighthouse.mjs --url=[staged URL]`173174If any check fails: refuse to ship, report what needs fixing, ask user how to proceed.175176### Step 9 — Ship per publishing method177178Read `site-info.md` → Publishing method:179180- **`repo-commit`** — write markdown/MDX file to the site's repo path, commit images to `public/blog/[slug]/`, open a PR (or commit directly to a branch named `blog/[slug]`). Output: PR URL.181- **`cms-paste`** — write to `sites/[site]/_drafts/[slug]/post.md` + `meta.json` (with title, meta description, OG tags, schema, image filenames). Output: clipboard-ready content + image folder path.182- **`lovable-prompt`** — generate a Lovable-ready prompt embedding the post content and structural instructions. Output: paste-ready prompt.183- **`headless-api`** — push via the configured API (Sanity / Contentful / Strapi). Output: CMS draft URL.184185### Step 10 — Update tracker186187Append to `sites/[site]/used-keywords.md`:188```markdown189| YYYY-MM-DD | [primary keyword] | Blog | [URL] | [cluster keywords comma-separated] |190```191192### Step 11 — Report193194Print a summary:195- Post title + URL196- Word count + image count197- Tier 1 status (all 16 ✅)198- Schema validation status199- Lighthouse score (if checked)200- Cluster keywords used201- Next recommended action (e.g., "Submit to GSC for indexing within 7 days")202203## Routine versioning + idempotency contract204205This skill participates in cruise-control via `--cron` mode. When invoked by cron:2062071. **Stamp `routine_version`** in every output produced (the `notes.md` audit entry header, `_inbox/` item frontmatter, draft folder metadata, audit log line written via `scripts/lib/audit-log.mjs`). Current `routine_version`: **1.0**. Bump when the skill's behavior meaningfully changes.2082092. **Idempotency:** this skill MUST be safe to run twice in a row on the same day without producing duplicate work. Implementation: check `_drafts/{YYYY-MM-DD}-*` at the start of every cron-mode run; if today's slug already exists, exit cleanly with `exit: "idempotent-skip"`.210211Reference: `docs/specs/2026-05-16-agents-cruise-control-design.md` § Operational hardening O3.212213## Refusal conditions214215The skill refuses to run if:216- Site folder doesn't exist or `site-info.md` missing.217- Resolved `references/` is incomplete (root files missing).218- `keywords.csv` is empty (and user didn't supply a primary keyword).219- Publishing this post would exceed cadence.220- Primary keyword already in `used-keywords.md` for this site.