Blog
Claude Code workspace template for running consistent SEO across a portfolio of managed websites.
npx -y skills add mkreindel/seo-toolkit-template --skill blogAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
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.
SKILL.md
16.4 KB, as published. Nobody here has run it
/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 itssite-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 viascripts/lib/audit-log.mjsappendRun({ 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/viascripts/lib/cron-mode.mjswriteInboxItem(...). After writing, exit cleanly withexit: "escalated". Do NOT useAskUserQuestionin cron mode. -
Defaults: when a choice would normally be asked, default to
site-info.md/goals.mdvalues. 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 })fromscripts/lib/audit-log.mjs. If true, the routine has hit the 3-strike threshold — write_inbox/routine-disabled-{name}.md, runscripts/sync-schedules.mjs --pause-routine={name}, and exit.
Step 1 — Read context (Rule 1)
Load in order:
CLAUDE.md(toolkit root)SEO_GUIDE.mdon-page-seo.mdsites/[site]/site-info.md- Resolved
references/— root files, then per-site overrides if present sites/[site]/keywords.csvandsites/[site]/used-keywords.mdsites/[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 inused-keywords.md, sort by opportunity score (high volume × low difficulty × intent match), pick top. - Seasonality check (soft) —
SEO_GUIDE.mdSection 2.5. If the picked keyword haspeak_monthsand/orseasonalitypopulated:- If today is in
peak_monthsORseasonality = stable→ proceed. - If
seasonality = seasonalorholiday-spikeAND today is NOT inpeak_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.
- If today is in
- 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.mddefault or per-run override). - Validate every cluster term against the primary using the SERP-comparison test (
SEO_GUIDE.mdSection 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.
- Search both keywords incognito (target region per
- 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.mdSection 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
/blogStep 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-sitereferences/voice.[lang].md(REQUIRED — refuse if missing) + per-sitereferences/humour.[lang].md/stories.[lang].md/opinions.[lang].md(optional).stats.mdis shared. - Format URL using the language's URL pattern from the Languages table (substitute
[slug]). - Reserve a draft folder:
sites/[site]/_drafts/[slug]/[lang]/.
- Resolve voice files: root
- Generate the hreflang link cluster from the resolved language set (self-reference + all siblings +
x-default). - Schema
inLanguageis 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 fromCLAUDE.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 (fromopinions.md, backed by a number fromstats.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.mdand 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-postauthor:frontmatter is REQUIRED and routed:author: your-name-slug(or any real-person slug) → loadsites/{site}/author-{slug}.md, inject the bio block in the page's author footer, injectPersonschema in@graph. Refuse to ship if any{TO FILL}placeholder remains inauthor-{slug}.md.author: contributor→ same as above, with the active contributor's slug per site-info.author: brand→ reference the site'sOrganization@idas author. NOPersonschema. Bio footer references the org's About page.- For sites where
site-info.mddeclares a single byline mode (e.g., site-b: always co-byline; site-c: always real-founder), the per-postauthor: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 viakeywords.csv/service-keywords.csv, look up category incoi-categories.md. If category appears inpersonal_eligible→author: your-name-slug. If category appears incoi_adjacentAND a contributor is onboarded →author: contributor. If category appears incoi_adjacentAND 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. PlusPersonIFFauthor: your-name-slug | contributor(a real person), OR Organization-as-author IFFauthor: brand. - Length within ±20% of SERP median.
- TOC + jump links if word count ≥ 1500 (
on-page-seo.mdCategory 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 topublic/blog/[slug]/, open a PR (or commit directly to a branch namedblog/[slug]). Output: PR URL.cms-paste— write tosites/[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_versionin every output produced (thenotes.mdaudit entry header,_inbox/item frontmatter, draft folder metadata, audit log line written viascripts/lib/audit-log.mjs). Currentroutine_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 withexit: "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.mdmissing. - Resolved
references/is incomplete (root files missing). keywords.csvis empty (and user didn't supply a primary keyword).- Publishing this post would exceed cadence.
- Primary keyword already in
used-keywords.mdfor this site.