Build a {{BRAND_NAME}} listicle / comparison blog end-to-end. Research live G2 ratings + pricing per tool via Firecrawl, capture hero screenshots, write deep per-tool sections (paragraphs + strengths / weaknesses / pricing bullets + clickable G2 link), generate a v6 cover image, draft a Google Doc for approval, push to your CMS as a draft with 5-entity JSON-LD @graph, run SEO preflight, publish only on explicit approval, update the Cluster Master Tracker. Trigger with `/listicle` or when the user asks to "build a listicle", "write a best-of-X blog", or "compare N tools" for {{BRAND_NAME}}.
This skill is brand-neutral. It reads its brand, palette and endpoints from
brand.config.json at the repo root.
On first use, do this before anything else:
Run python3 brandkit.py. It prints the config source and any placeholder
that is still unset.
If it says configured: False, copy brand.config.example.json to
brand.config.json.
Ask the operator for each value under missing, then write them in. Do not
guess a brand name, a domain, or a colour.
Anything the skill writes out should be passed through
brandkit.fill(text), which swaps every {{TOKEN}} for its configured value
and remaps the default palette to the operator's.
Text below uses {{TOKEN}} where a value is operator-specific. Treat an
unresolved {{TOKEN}} in your output as a bug, not as literal copy.
Publishing: which CMS
Never write CMS calls by hand in this skill. Use the shared adapter layer, which
covers Strapi, WordPress, Contentful, Sanity, Ghost, Webflow, Payload, Directus,
and plain files.
from cms import get_adapter, Page, CMSError
cms = get_adapter() # reads cms.kind from brand.config.json
page = Page(slug=slug, title=title, description=meta_desc,
html=body_html, blocks=components, jsonld=graph)
entry_id = cms.upsert(page) # ALWAYS a draft, whatever the CMS
admin, public = cms.locate(page, entry_id)
Rules:
upsert() creates a draft. There is no way to publish through it, by design.
cms.publish(page) goes live. Call it only after the operator says so, for
this page, in this session. Approval never carries forward.
If cms.kind is none, get_adapter() raises. Report that and offer files,
which writes the page to disk instead, rather than guessing a CMS.
python3 -c "import cms; print(cms.describe())" tells the operator what is
wired and whether that adapter has been verified live.
Only strapi and files are verified against real instances. For the rest,
build the page, run the audit, then tell the operator to expect a possible
field-name mismatch on the first push.
{{BRAND_NAME}} Listicle Blog Builder
Ships a listicle-style {{BRAND_NAME}} blog (Best X software / tools in <year>: Compared) end-to-end. Every step is aligned with the mandatory {{BRAND_NAME}} content rules: no em-dashes, no hardcoded dates, no fabricated competitor claims, no publish without approval, 5-entity JSON-LD @graph, screenshots + G2 links for legitimacy, verifiable pricing.
The reference implementation is the 4 listicles shipped 2026-07-10 (best-data-collection-software, best-quiz-funnel-software, best-questionnaire-software, best-poll-maker). Working scripts from that ship live under ~/.claude/skills/listicle/scripts/.
Rule 0 — Inputs (ask one at a time, in this order)
Do NOT skip any input. Echo the full block back for confirmation before starting work.
Primary keyword — the exact search phrase the listicle targets, e.g., best data collection software.
Tool list — the N tools to cover (in ranking order). Include {{BRAND_NAME}} at position 1 unless the user overrides. For each tool: display name (respecting brand casing per competitor-research.md) and the tool's marketing URL (used for the hero screenshot).
Blog angle per tool — a one-line "best for X" per tool (e.g., best all-around, best for feature depth, best for offline field data collection).
Meta title — 30-60 characters, ends with | {{BRAND_NAME}}.
Meta description — 120-160 characters. Descriptive, no marketing fluff.
Description — a longer 2-3 sentence blurb for your CMS description field (shown in blog cards).
Author — defaults to Harish Kumar with LinkedIn https://www.linkedin.com/in/harish-kumar2424/. Ask if it differs.
Inspirational page — a URL of a competing listicle doing well in search or LLM citations (for the tracker).
Sub-cluster + PK volume — for the tracker row (Sub cluster column and PK SV (US) column in Cluster Master Tracker).
After collection, echo the whole block back and ask for explicit confirmation before proceeding.
Rule 0.5 — Phase 0 SERP + LLM ranking-factor recon (mandatory, BEFORE any content build)
After Rule 0 collects inputs and BEFORE any other rule fires, run the full Phase 0 SERP + LLM ranking-factor recon per ~/.claude/skills/_shared/serp_llm_recon.md.
Four steps:
Google SERP top-10 via DataForSEO Live SERP (US, desktop, depth 10). Extract top-10 organic, SERP features (AI Overview, PAA, Video pack, Related Searches, Discussions & Forums), and PAA questions verbatim.
Firecrawl top 5 commercial competitors (skip academic .edu unless the topic warrants it). Extract H2/H3 tree, word count, entity coverage, EEAT signals, distinctive sections our default template misses.
LLM citation probe via OpenAI gpt-4o-search-preview × 4 query variants (informational / free / format-specific / comparison). Parse annotations[].url_citation for cited domains + track {{BRAND_NAME}} body mentions.
Synthesize — present the ranking-factor report to the user with: current state, SERP snapshot, competitor pattern, LLM citation status, ranking factors to take, ranking factors to add ({{BRAND_NAME}} differentiators), proposed component/section plan, meta title + description, HIPAA framing decision.
Wait for explicit user approval of the synthesis before proceeding to the actual build. The user can adjust tool shortlists, personas, positioning, or scope at this stage — much cheaper than after 10-13 components are drafted.
Never skip. Every content skill that shipped without this step in 2026-07 landed with 0/4 LLM citations, wrong-intent-bucket SERP positioning, and no competitor pattern insight (documented in the 11-page audit 2026-07-31 + the /listicle post-mortems). Rule 0.5 exists to prevent this exact class of miss.
Reference implementations:consent-form-generator (page 898, shipped 2026-08-03) — the first page with this baked in. Scratchpad artifacts at <repo>/scratchpad/consent-form-generator-phase0/. cognito-forms-alternative (page 896, upgraded 2026-08-03) — first alt-page upgrade using this protocol.
Rule 1 — Research phase (Firecrawl + competitor-research.md)
Before writing a single word of the listicle, run parallel research to produce a data pack.
G2 ratings (mandatory)
For each tool, fetch https://www.g2.com/products/<slug>/reviews via Firecrawl formats: ["markdown"]. Extract the rating using the ###### X.X out of 5 stars anchor, NOT the first X.X/5(N) match (that's the sponsored "Hive" ad G2 shows at the top of every product page). If the extracted review count is 667, that's Hive's — pull the next match.
If a tool has no G2 profile (e.g., StrawPoll), do NOT fabricate one. Use this line instead:
<p><strong>G2 rating:</strong> Not listed on G2 (consumer-facing tool)</p>
For all other tools, use a real clickable link:
<p><strong>G2 rating:</strong> <a href="https://www.g2.com/products/<slug>/reviews" target="_blank" rel="noopener">4.7/5 (11 reviews on G2)</a></p>
Pricing (mandatory)
For each tool:
If the tool is in competitor-research.md (Fillout, Jotform, Gravity Forms, WPForms, Microsoft Forms, Zoho Forms, forms.app, Google Forms), pull pricing from the dossier.
Otherwise, Firecrawl the tool's /pricing/ page. If the pricing page 404s or renders empty (JS-heavy pages), try /plans or /pricing (no trailing slash) as a fallback.
If the number is still not confirmed, ask the user before writing it. Never invent competitor pricing.
Hero screenshots (mandatory)
For each tool, Firecrawl the homepage with formats: ["screenshot"], waitFor: 2500 to get a viewport (nav + hero) shot at 1440×900. Save to <slug>/shots/<tool-slug>.png. Spot-check 2-3 for cookie-banner overlays — if a cookie modal blocks the hero, retry with a longer waitFor or use /product as the target URL.
Data pack output
Save the research pack as one JSON at <scratchpad>/<slug>/research.json:
{
"tools": {
"jotform": {
"name": "Jotform",
"url": "https://www.jotform.com/",
"g2_url": "https://www.g2.com/products/jotform/reviews",
"g2_rating": "4.7/5",
"g2_reviews": "5,321",
"pricing": {
"free": "5 forms, 100 submissions/month, 100 MB storage",
"starting": "Bronze at $34/month (annual $24)",
"most_popular": "Silver at $39/month (annual $29), unlocks HIPAA add-on",
"enterprise": "Custom pricing with SSO, dedicated support, signed BAA",
"free_trial": "Not needed; the free plan is permanent"
}
}
}
}
Anything a section needs must be sourced from this file or the dossier. Do not paraphrase from memory.
Rule 2 — Content structure per tool section (locked)
Every tool section follows this exact structure. Vary the content, not the shape.
<h2>N. <Tool Name>, <best for X></h2>
<p><img src="<hero-shot-url>" alt="<Tool Name> homepage hero screenshot" style="width:100%;max-width:900px;height:auto;border-radius:8px;border:1px solid #e5e7eb;margin:16px 0;" loading="lazy" /></p>
<p>Paragraph 1 (opens the section — 1 to 2 lines maximum).</p>
<p>Paragraph 2.</p>
<p>Paragraph 3 (optional; 3-5 short paras total per tool).</p>
<p><strong>Best for:</strong> <specific one-line criterion combining role + use case + constraint>.</p>
<p><strong>Strengths</strong></p>
<ul>
<li>Bullet 1 (concrete, cites a named feature, integration, or limit)</li>
<li>Bullet 2</li>
...
</ul>
<p><strong>Weaknesses</strong></p>
<ul>
<li>Honest, tool-specific weakness (not recycled hedges)</li>
<li>...</li>
</ul>
<p><strong>Pricing</strong></p>
<ul>
<li><strong>Free plan:</strong> <what the free tier ships, with caps>.</li>
<li><strong>Starting paid tier:</strong> <plan name at $N/month, what it adds>.</li>
<li><strong>Most popular:</strong> <plan name at $N/month, what it adds>.</li>
<li><strong>Enterprise:</strong> <Custom pricing with SSO / BAA / etc.>.</li>
<li><strong>Free trial:</strong> <length or "Not needed; free plan is permanent">.</li>
</ul>
<p><strong>G2 rating:</strong> <a href="<g2-url>" target="_blank" rel="noopener"><rating> (<reviews> on G2)</a></p>
Hard rules on the copy
Every paragraph ≤ 2 lines at typical blog width (~800px, ~100 chars/line ≈ 200 chars max). If a paragraph runs longer, split it.
3-5 paragraphs per tool (vary the count; if all tools have exactly 3, the AI-tell is loud).
Vary bullet counts: {{BRAND_NAME}} might get 6-7 strengths, Google Forms 4-5, StrawPoll 5. Do NOT lock every tool at N=5 bullets.
Weaknesses are honest and tool-specific: small G2 review counts, missing HIPAA, price jumps, dated UI, feature gaps. Recycled hedges ("newer to market", "smaller ecosystem") across every tool is the classic AI tell.
Every claim is sourced. Feature names come from the tool's own page or competitor-research.md. Pricing comes from the research pack. G2 numbers come from Firecrawl.
{{BRAND_NAME}} claims trace back to brand-facts.md — never invent features {{BRAND_NAME}} doesn't ship. Current caveats: no HIPAA, no SOC 2, no ISO 27001 (roadmap items).
First paragraph of the FIRST tool section (usually {{BRAND_NAME}} at position 1) gets <p style="font-size: inherit;"> per the blog lead paragraph rule.
The first tool (usually {{BRAND_NAME}}) sits at position 1 unless the user overrides
The first tool section is the anchor: 5 short paragraphs (max), highest specificity, ends with the strongest "Best for" line. Every reader who bounces past the first tool never sees the rest, so this section carries the most weight.
Rule 3 — Voice / AI-tell prevention (mandatory)
Listicles are the format most likely to read as AI-generated. Kill the tells:
Vary paragraph counts across tools. 3 paras for a simpler tool, 5 for a nuanced one. Never identical structure across all N.
Vary bullet counts. As above.
Vary the opening sentence pattern. Some sections open with a market-position claim ("X is the category leader for Y"), some with a specific use case ("For anyone collecting more than a hundred responses a month..."), some with a strong claim ("X is the only builder on this list that ships an uncapped free plan"), some with a comparison ("X and Y both target creators, but the sweet spot differs.").
Vary sentence length. Mix a 3-word landing sentence with a 20-word sentence. Symmetric parallelism across every paragraph is the biggest tell.
Reference concrete artifacts where possible: exact plan names, integration counts, file-size caps, review-quote fragments, specific product features. Not generic phrases like "clean interface" or "strong integrations."
Strip near-synonym pairings: "quick and efficient," "simple and straightforward," "fast, easy, and reliable."
Strip hollow imperatives: "Empower your team to…" "Unlock the power of…"
Rule 4 — Global "How we verified this" note
At the top of every listicle (immediately before the first non-hero <h2> — usually The 60-second TL;DR or The quick pick), insert one global data-verification callout. Never per-tool.
<div style="background:#F9FAFB;border-left:4px solid #7f56d9;padding:14px 18px;margin:0 0 24px 0;border-radius:6px;font-family:inherit;">
<p style="margin:0;font-size:14px;color:#475467;font-family:inherit;"><strong>How we verified this:</strong> ratings, review counts, and prices sourced directly from each tool's own pricing / product pages and their public G2 review profile on <YYYY-MM-DD>. We rechecked every price and free-tier cap the morning of publish. Live listings may vary if a tool updates pricing after this date.</p>
</div>
The date substitutes to build-time today(). Never a hardcoded string (per feedback_no_hardcoded_dates.md).
Rule 5 — Body scaffolding (existing Minimal Structure conventions apply)
Everything else about the body follows the standing blog rules:
Minimal Structure mode (feedback_blog_minimal_structure_mode.md): semantic HTML + fixed class catalog, no @import fonts, font-family: inherit, <p style="font-size: inherit;"> on the lead paragraph.
FAQs in <section class="faq"><details><summary>Q</summary><div>A</div></details></section> shape. Never external links inside FAQ answers.
Tables wrapped in <div class="table-wrap">.
Mid-article violet CTA card at 40-60% depth, before a meaningful H2 (per feedback_blog_tailored_cta.md). Copy is tailored per listicle — do not reuse the same CTA across all four.
No em-dashes anywhere (per feedback_no_em_dashes_hard_rule.md). If a rewrite pipeline emits em-dashes, the preflight strips them, but treat that as a bug in the pipeline, not a working state.
No repeat internal links in body (per feedback_no_repeat_internal_links.md).
Rule 6 — Cover image
Every listicle gets a v6 cover via /cover-image:
Headline lines split so the primary phrase gets an orange accent
Illustration subject describes what the cream-card panel should show (a clipboard + form + chart for data collection, a quiz card interface for quiz funnels, etc.)
Wordmark top-center, small (4% canvas height, 4.5% top pad) — this is the v6 default; no override needed
After generation, upload the PNG to your CMS media library, set as coverImg on the blog, prepend  to the body (powers the in-page hero), and set the image field on the Article node in the JSON-LD @graph.
Rule 7 — Google Doc draft (approval gate before the CMS push)
Per feedback_drafts_always_in_gdoc.md: never push to your CMS without a Google Doc draft first.
Metadata table first (Title, Description, Meta title, Meta description, Primary keyword, Primary keyword volume (US), Secondary keywords, Slug, Recommended URL, CMS link). No other rows.
Body: verbatim listicle copy in reading order — intro, [H2] per tool, [H3] sub-sections inside each tool (Overview / Pricing / Strengths / Weaknesses / Best for), comparison table as a real Docs table, FAQ + related reading at the end. Every heading tagged [H1] / [H2] / [H3] inline. Every image marker on its own line as [Image: alt text].
No commentary, no rationale, no cover URL or hero-screenshot URLs, no G2 API-response JSON, no scoring rubric explanation. Pricing / ratings / links land in body text or the comparison table verbatim.
Convert the assembled spec via ~/.claude/skills/gdoc/build_gdoc.py with GDOC_AUTH_DIR={{GOOGLE_AUTH_DIR}} ({{GOOGLE_ACCOUNT}} OAuth). The skill handles Geist typography and real Docs tables under the hood.
When updating an existing listicle, regenerate the Doc in place via --doc-id <existing_id>.
Share the Doc URL back to the user; wait for approval before CMS push
After Google Doc approval, push to your CMS as a DRAFT. Never publish without a separate explicit approval (per feedback_never_publish_without_approval.md).
Payload shape for POST /api/blogs:
{
"data": {
"title": "<meta title without | {{BRAND_NAME}}>",
"description": "<longer blurb for blog cards>",
"metaTitle": "<meta title 30-60 chars>",
"metaDescription": "<meta desc 120-160 chars>",
"keywords": "<comma-separated>",
"author": "Harish Kumar",
"authorProfile": "https://www.linkedin.com/in/harish-kumar2424/",
"coverImg": <media_id>,
"coverImgAlt": "<alt text>",
"featured": false,
"slug": "<slug>",
"body": "<full HTML body>",
"schema": [ ... 6 entities ... ],
"publishedAt": null
}
}
Duplicate check: before POST, query GET /api/blogs?filters[slug][$eq]=<slug>&fields[0]=slug&publicationState=preview. Skip if it already exists (may be a re-run).
Rule 9 — 5-entity (6-node) JSON-LD @graph
Every listicle ships with the mandatory graph (per feedback_seo_publish_checklist.md):
Organization — {{BRAND_NAME}}, referenced by @id
WebPage — datePublished, dateModified, headline (verbatim from title), inLanguage
BreadcrumbList — Home > Blog >
Article — headline, description, datePublished, dateModified, author, publisher, mainEntityOfPage, image (cover URL, set after cover upload)
FAQPage — every FAQ from the body, verbatim
SoftwareApplication — {{BRAND_NAME}} (the subject product), with offers array (Free at $0, Personal at $13) and aggregateRating (4.7/5, 11 reviews on G2)
Dates resolve to today() at build time. Never a hardcoded date string.
Global "How we verified this" note is present at the top
Cover image is uploaded, coverImg is set, and body prepends  for the in-page hero
Every tool section has a hero screenshot, Strengths bullets, Weaknesses bullets, Pricing bullets, and a G2 rating line (real link or "Not listed on G2" fallback)
Every claim about competitors traces to competitor-research.md or a Firecrawl fetch documented in research.json
Every claim about {{BRAND_NAME}} traces to brand-facts.md
MANDATORY hygiene preflight — call run_preflight() from ~/.claude/skills/_shared/preflight.py inside the build/push code path BEFORE building the CMS payload, and persist the result on disk so the source-of-truth stays in sync. Wired into scripts/build_listicle_bodies.py 2026-07-27.
Runs three scrubbers in order: strip_trailing_periods (no periods on headings, badges, CTAs), fix_button_hover_color (button :hover keeps base color with !important — never inherits to browser-default black), fix_markdown_leakage (catches **bold** markers the md→html converter missed, unwraps <em> blocks that swallowed paragraph body as italic, merges numbered sub-items into the canonical <p><strong>Label.</strong> Body</p> shape, rebuilds malformed <strong> tags). Body paragraphs and FAQ answers keep their periods; headings + __title / __badge / __eyebrow / __pill / __label / __cta classes do not. Rationale:/tally-vs-typeform/ shipped 2026-07-26 with heading periods; Sign Up Free button shipped with hover text dropping to black; reviews 1855-1858 shipped with markdown leaks — every one because bespoke adapter push scripts skipped the checks. Never again.
Audit-scan preflight (BLOCKS publish). Run python3 ~/.claude/skills/_shared/audit_scan.py <body.html> on the assembled listicle body. Exit 1 (critical) = do not push, fix and re-run. Exit 2 (high) = treat as blocker unless operator overrides. Catches: {{BRAND_NAME}} HIPAA/SOC 2/ISO overclaim, {{BRAND_NAME}} stale caps, "(verify)" placeholder leaks, recycled offline-field-teams Strengths block signatures via Razorpay/HIPAA-roadmap phrasing, hardcoded date strings, em-dashes, spaced-comma typos. Rationale: best-quiz-funnel-software and best-poll-maker shipped 2026-07-15 with literal (verify) in copy, cross-tool plan-name contamination, and recycled Strengths blocks; both had to be unpublished 24 hours later.
Rule 11 — Publish (only on explicit approval)
Per feedback_never_publish_without_approval.md: never flip publishedAt without explicit user approval on the current blog. Prior approvals do not carry forward.
When approved:
Set publishedAt to datetime.now(timezone.utc).isoformat() (Z suffix, no microseconds)
Confirm the image field is now populated on the Article schema node
PUT the payload to PUT /api/blogs/<id>
Poll {{SITE_URL}}/blog/<slug>/ for 200. Expect a delay of 5-15 min for the static rebuild. If it stays 404 past 15 min, flag it and ask the user whether to trigger the Netlify/Vercel build hook.
Rule 12 — Cluster Master Tracker update
After the CMS draft is created, append a row to the tracker.
Column semantics per feedback_master_tracker_locked_schema.md
On draft push: Status = Ready, Live URL = CMS admin URL ({{CMS_ADMIN_URL}}/api::blog.blog/<id>), Notes = draft summary.
On publish approval: flip Status to Published, Live Date + Publish date to today(), Live URL to {{SITE_URL}}/blog/<slug>/, refresh Notes to reflect ship state.
Rule 13 — Reference implementation
The 4 listicles shipped 2026-07-10 are the reference. Working scripts live under ~/.claude/skills/listicle/scripts/:
build_listicle_bodies.py — markdown → Minimal Structure body HTML, injects tailored CTA per slug, strips em-dashes, wraps lead in font-size: inherit
embed_listicle_shots.py — Firecrawl viewport screenshots → your CMS media library → inject <img> after each tool H2
rewrite_all_sections.py — orchestrator for the deep-section rewrite (paragraphs + strengths + weaknesses + pricing + G2)
gen_covers_top_center.py — helper wrapper for /cover-image v6 (top-center small wordmark); can be inlined since v6 is now the default
Never invent competitor pricing. Always source from competitor-research.md or a Firecrawl fetch. Anything ambiguous asks the user.
Never fabricate G2 ratings. If a tool has no G2 profile, use the "Not listed on G2" line. Never invent a number.
Never claim {{BRAND_NAME}} features that aren't shipped. Cross-check brand-facts.md. {{BRAND_NAME}} is NOT HIPAA-certified, NOT SOC 2, NOT ISO 27001 — do not soften these into "compliant with major frameworks."
Never use em-dashes anywhere in the body, FAQ answers, headlines, or alt text.
Never leave trailing full stops on headings, badges, CTAs, feature titles, or short UI copy. Headings like "Free.", "Easy.", "Fast." or "{{BRAND_NAME}} vs X: the honest comparison." all drop the period. Body paragraphs and FAQ answers keep periods (they are full sentences). Run ~/.claude/skills/_shared/strip_trailing_periods.py before every push.
Never hardcode dates in generated components. All dates resolve to today() at build time.
Never publish without explicit user approval on the current blog. Prior approvals do not carry forward.
Never skip the Google Doc approval step. CMS drafts get pushed AFTER the user has approved the Doc.
Never use the wrong OAuth token. Sheets/GSC use the .env refresh-token OAuth; Docs/Drive use {{GOOGLE_AUTH_DIR}}/token.json. Both under {{GOOGLE_ACCOUNT}}. Never {{GOOGLE_ACCOUNT}}.
Never write "Data verified on " per tool anywhere in the body. That goes in one global "How we verified this" callout at the top.
No self-referential media captions (HARD RULE, added 2026-08-09)
Never add authenticity remarks under or near screenshots, GIFs, or videos: "Real {{BRAND_NAME}} UI, captured in-app", "actual product screenshot", "real UI, not a mockup", "captured from the live app", or any similar meta-commentary about the asset being real. These read as AI-generated filler and were ordered removed site-wide. The asset speaks for itself. If a media element genuinely needs a caption, the caption states only WHAT the user is looking at (e.g. "Form builder, Design tab"), never that it is real, actual, or captured. This applies to every page type: product, landing, alternative, blog, listicle, review, template.
Inspiration pages are design-only, never fact sources (GLOBAL HARD RULE, added 2026-08-10)
Root cause: the Aug 2026 factual audit found 4 alternative pages shipped with the inspiration page's facts (Wufoo's pricing tiers, ownership, founding year) left in place for different competitors. This must never recur.
An internal inspiration/reference page may be reused ONLY for: page structure, section structure, layout, design patterns, visual treatment, formatting, component patterns, UX approach.
NEVER carry over content, claims, facts, statistics, pricing, plan names, limits, ownership, founding dates, quotes, or any information from the inspiration page into a new page.
Research comes FIRST on every new page: (1) research the subject independently, (2) identify current correct information, (3) verify every factual claim against first-party/authoritative sources (official pricing page, docs, company newsroom), (4) write original content from that research.
Never assume something is correct because it appears on an existing {{BRAND_NAME}} page. Never fill gaps with guesses. If a claim cannot be verified, flag it explicitly instead of presenting it as fact.
Cloned template blocks that carry competitor-specific facts (pricing tables, "why teams leave" cards, honest-comparison cards, free-plan tables) must be rebuilt from fresh research for every new subject, not find-and-replaced.
1---2name: listicle3description: Build a {{BRAND_NAME}} listicle / comparison blog end-to-end. Research live G2 ratings + pricing per tool via Firecrawl, capture hero screenshots, write deep per-tool sections (paragraphs + strengths / weaknesses / pricing bullets + clickable G2 link), generate a v6 cover image, draft a Google Doc for approval, push to your CMS as a draft with 5-entity JSON-LD @graph, run SEO preflight, publish only on explicit approval, update the Cluster Master Tracker. Trigger with `/listicle` or when the user asks to "build a listicle", "write a best-of-X blog", or "compare N tools" for {{BRAND_NAME}}.4---5<!-- SETUP:BEGIN -->6## Before you run this skill78This skill is brand-neutral. It reads its brand, palette and endpoints from9`brand.config.json` at the repo root.1011**On first use, do this before anything else:**12131. Run `python3 brandkit.py`. It prints the config source and any placeholder14 that is still unset.152. If it says `configured: False`, copy `brand.config.example.json` to16 `brand.config.json`.173. Ask the operator for each value under `missing`, then write them in. Do not18 guess a brand name, a domain, or a colour.194. Anything the skill writes out should be passed through20 `brandkit.fill(text)`, which swaps every `{{TOKEN}}` for its configured value21 and remaps the default palette to the operator's.2223Text below uses `{{TOKEN}}` where a value is operator-specific. Treat an24unresolved `{{TOKEN}}` in your output as a bug, not as literal copy.2526<!-- SETUP:END -->2728<!-- CMS:BEGIN -->29## Publishing: which CMS3031Never write CMS calls by hand in this skill. Use the shared adapter layer, which32covers Strapi, WordPress, Contentful, Sanity, Ghost, Webflow, Payload, Directus,33and plain files.3435```python36from cms import get_adapter, Page, CMSError3738cms = get_adapter() # reads cms.kind from brand.config.json39page = Page(slug=slug, title=title, description=meta_desc,40 html=body_html, blocks=components, jsonld=graph)4142entry_id = cms.upsert(page) # ALWAYS a draft, whatever the CMS43admin, public = cms.locate(page, entry_id)44```4546Rules:4748- `upsert()` creates a draft. There is no way to publish through it, by design.49- `cms.publish(page)` goes live. Call it **only** after the operator says so, for50 this page, in this session. Approval never carries forward.51- If `cms.kind` is `none`, `get_adapter()` raises. Report that and offer `files`,52 which writes the page to disk instead, rather than guessing a CMS.53- `python3 -c "import cms; print(cms.describe())"` tells the operator what is54 wired and whether that adapter has been verified live.55- Only `strapi` and `files` are verified against real instances. For the rest,56 build the page, run the audit, then tell the operator to expect a possible57 field-name mismatch on the first push.5859<!-- CMS:END -->6061# {{BRAND_NAME}} Listicle Blog Builder6263Ships a listicle-style {{BRAND_NAME}} blog (`Best X software / tools in <year>: Compared`) end-to-end. Every step is aligned with the mandatory {{BRAND_NAME}} content rules: no em-dashes, no hardcoded dates, no fabricated competitor claims, no publish without approval, 5-entity JSON-LD @graph, screenshots + G2 links for legitimacy, verifiable pricing.6465The reference implementation is the 4 listicles shipped 2026-07-10 (`best-data-collection-software`, `best-quiz-funnel-software`, `best-questionnaire-software`, `best-poll-maker`). Working scripts from that ship live under `~/.claude/skills/listicle/scripts/`.6667---6869## Rule 0 — Inputs (ask one at a time, in this order)7071Do NOT skip any input. Echo the full block back for confirmation before starting work.72731. **Primary keyword** — the exact search phrase the listicle targets, e.g., `best data collection software`.742. **Slug** — kebab-case URL slug, e.g., `best-data-collection-software`.753. **Tool list** — the N tools to cover (in ranking order). Include {{BRAND_NAME}} at position 1 unless the user overrides. For each tool: display name (respecting brand casing per `competitor-research.md`) and the tool's marketing URL (used for the hero screenshot).764. **Blog angle per tool** — a one-line "best for X" per tool (e.g., `best all-around`, `best for feature depth`, `best for offline field data collection`).775. **Meta title** — 30-60 characters, ends with `| {{BRAND_NAME}}`.786. **Meta description** — 120-160 characters. Descriptive, no marketing fluff.797. **Description** — a longer 2-3 sentence blurb for your CMS `description` field (shown in blog cards).808. **Author** — defaults to `Harish Kumar` with LinkedIn `https://www.linkedin.com/in/harish-kumar2424/`. Ask if it differs.819. **Inspirational page** — a URL of a competing listicle doing well in search or LLM citations (for the tracker).8210. **Sub-cluster + PK volume** — for the tracker row (Sub cluster column and PK SV (US) column in `Cluster Master Tracker`).8384After collection, echo the whole block back and ask for explicit confirmation before proceeding.8586---8788## Rule 0.5 — Phase 0 SERP + LLM ranking-factor recon (mandatory, BEFORE any content build)8990After Rule 0 collects inputs and BEFORE any other rule fires, run the full Phase 0 SERP + LLM ranking-factor recon per `~/.claude/skills/_shared/serp_llm_recon.md`.9192**Four steps:**93941. **Google SERP top-10** via DataForSEO Live SERP (US, desktop, depth 10). Extract top-10 organic, SERP features (AI Overview, PAA, Video pack, Related Searches, Discussions & Forums), and PAA questions verbatim.952. **Firecrawl top 5 commercial competitors** (skip academic `.edu` unless the topic warrants it). Extract H2/H3 tree, word count, entity coverage, EEAT signals, distinctive sections our default template misses.963. **LLM citation probe** via OpenAI `gpt-4o-search-preview` × 4 query variants (informational / free / format-specific / comparison). Parse `annotations[].url_citation` for cited domains + track {{BRAND_NAME}} body mentions.974. **Synthesize** — present the ranking-factor report to the user with: current state, SERP snapshot, competitor pattern, LLM citation status, ranking factors to take, ranking factors to add ({{BRAND_NAME}} differentiators), proposed component/section plan, meta title + description, HIPAA framing decision.9899**Wait for explicit user approval of the synthesis before proceeding to the actual build.** The user can adjust tool shortlists, personas, positioning, or scope at this stage — much cheaper than after 10-13 components are drafted.100101**Never skip.** Every content skill that shipped without this step in 2026-07 landed with 0/4 LLM citations, wrong-intent-bucket SERP positioning, and no competitor pattern insight (documented in the 11-page audit 2026-07-31 + the /listicle post-mortems). Rule 0.5 exists to prevent this exact class of miss.102103**Reference implementations:** `consent-form-generator` (page 898, shipped 2026-08-03) — the first page with this baked in. Scratchpad artifacts at `<repo>/scratchpad/consent-form-generator-phase0/`. `cognito-forms-alternative` (page 896, upgraded 2026-08-03) — first alt-page upgrade using this protocol.104105---106107## Rule 1 — Research phase (Firecrawl + competitor-research.md)108109**Before writing a single word** of the listicle, run parallel research to produce a data pack.110111### G2 ratings (mandatory)112113For each tool, fetch `https://www.g2.com/products/<slug>/reviews` via Firecrawl `formats: ["markdown"]`. Extract the rating using the `###### X.X out of 5 stars` anchor, NOT the first `X.X/5(N)` match (that's the sponsored "Hive" ad G2 shows at the top of every product page). If the extracted review count is 667, that's Hive's — pull the next match.114115If a tool has no G2 profile (e.g., StrawPoll), do NOT fabricate one. Use this line instead:116117```html118<p><strong>G2 rating:</strong> Not listed on G2 (consumer-facing tool)</p>119```120121For all other tools, use a real clickable link:122123```html124<p><strong>G2 rating:</strong> <a href="https://www.g2.com/products/<slug>/reviews" target="_blank" rel="noopener">4.7/5 (11 reviews on G2)</a></p>125```126127### Pricing (mandatory)128129For each tool:1301. If the tool is in `competitor-research.md` (Fillout, Jotform, Gravity Forms, WPForms, Microsoft Forms, Zoho Forms, forms.app, Google Forms), pull pricing from the dossier.1312. Otherwise, Firecrawl the tool's `/pricing/` page. If the pricing page 404s or renders empty (JS-heavy pages), try `/plans` or `/pricing` (no trailing slash) as a fallback.1323. If the number is still not confirmed, ask the user before writing it. **Never invent competitor pricing.**133134### Hero screenshots (mandatory)135136For each tool, Firecrawl the homepage with `formats: ["screenshot"], waitFor: 2500` to get a viewport (nav + hero) shot at 1440×900. Save to `<slug>/shots/<tool-slug>.png`. Spot-check 2-3 for cookie-banner overlays — if a cookie modal blocks the hero, retry with a longer `waitFor` or use `/product` as the target URL.137138### Data pack output139140Save the research pack as one JSON at `<scratchpad>/<slug>/research.json`:141142```json143{144 "tools": {145 "jotform": {146 "name": "Jotform",147 "url": "https://www.jotform.com/",148 "g2_url": "https://www.g2.com/products/jotform/reviews",149 "g2_rating": "4.7/5",150 "g2_reviews": "5,321",151 "pricing": {152 "free": "5 forms, 100 submissions/month, 100 MB storage",153 "starting": "Bronze at $34/month (annual $24)",154 "most_popular": "Silver at $39/month (annual $29), unlocks HIPAA add-on",155 "enterprise": "Custom pricing with SSO, dedicated support, signed BAA",156 "free_trial": "Not needed; the free plan is permanent"157 }158 }159 }160}161```162163Anything a section needs must be sourced from this file or the dossier. Do not paraphrase from memory.164165---166167## Rule 2 — Content structure per tool section (locked)168169Every tool section follows this exact structure. Vary the content, not the shape.170171```html172<h2>N. <Tool Name>, <best for X></h2>173174<p><img src="<hero-shot-url>" alt="<Tool Name> homepage hero screenshot" style="width:100%;max-width:900px;height:auto;border-radius:8px;border:1px solid #e5e7eb;margin:16px 0;" loading="lazy" /></p>175176<p>Paragraph 1 (opens the section — 1 to 2 lines maximum).</p>177178<p>Paragraph 2.</p>179180<p>Paragraph 3 (optional; 3-5 short paras total per tool).</p>181182<p><strong>Best for:</strong> <specific one-line criterion combining role + use case + constraint>.</p>183184<p><strong>Strengths</strong></p>185186<ul>187<li>Bullet 1 (concrete, cites a named feature, integration, or limit)</li>188<li>Bullet 2</li>189...190</ul>191192<p><strong>Weaknesses</strong></p>193194<ul>195<li>Honest, tool-specific weakness (not recycled hedges)</li>196<li>...</li>197</ul>198199<p><strong>Pricing</strong></p>200201<ul>202<li><strong>Free plan:</strong> <what the free tier ships, with caps>.</li>203<li><strong>Starting paid tier:</strong> <plan name at $N/month, what it adds>.</li>204<li><strong>Most popular:</strong> <plan name at $N/month, what it adds>.</li>205<li><strong>Enterprise:</strong> <Custom pricing with SSO / BAA / etc.>.</li>206<li><strong>Free trial:</strong> <length or "Not needed; free plan is permanent">.</li>207</ul>208209<p><strong>G2 rating:</strong> <a href="<g2-url>" target="_blank" rel="noopener"><rating> (<reviews> on G2)</a></p>210```211212### Hard rules on the copy213214- **Every paragraph ≤ 2 lines** at typical blog width (~800px, ~100 chars/line ≈ 200 chars max). If a paragraph runs longer, split it.215- **3-5 paragraphs per tool** (vary the count; if all tools have exactly 3, the AI-tell is loud).216- **Vary bullet counts:** {{BRAND_NAME}} might get 6-7 strengths, Google Forms 4-5, StrawPoll 5. Do NOT lock every tool at N=5 bullets.217- **Weaknesses are honest and tool-specific:** small G2 review counts, missing HIPAA, price jumps, dated UI, feature gaps. Recycled hedges ("newer to market", "smaller ecosystem") across every tool is the classic AI tell.218- **Every claim is sourced.** Feature names come from the tool's own page or `competitor-research.md`. Pricing comes from the research pack. G2 numbers come from Firecrawl.219- **{{BRAND_NAME}} claims** trace back to `brand-facts.md` — never invent features {{BRAND_NAME}} doesn't ship. Current caveats: no HIPAA, no SOC 2, no ISO 27001 (roadmap items).220- **First paragraph of the FIRST tool section** (usually {{BRAND_NAME}} at position 1) gets `<p style="font-size: inherit;">` per the blog lead paragraph rule.221222### The first tool (usually {{BRAND_NAME}}) sits at position 1 unless the user overrides223224The first tool section is the anchor: 5 short paragraphs (max), highest specificity, ends with the strongest "Best for" line. Every reader who bounces past the first tool never sees the rest, so this section carries the most weight.225226---227228## Rule 3 — Voice / AI-tell prevention (mandatory)229230Listicles are the format most likely to read as AI-generated. Kill the tells:231232- **Vary paragraph counts across tools.** 3 paras for a simpler tool, 5 for a nuanced one. Never identical structure across all N.233- **Vary bullet counts.** As above.234- **Vary the opening sentence pattern.** Some sections open with a market-position claim ("X is the category leader for Y"), some with a specific use case ("For anyone collecting more than a hundred responses a month..."), some with a strong claim ("X is the only builder on this list that ships an uncapped free plan"), some with a comparison ("X and Y both target creators, but the sweet spot differs.").235- **Vary sentence length.** Mix a 3-word landing sentence with a 20-word sentence. Symmetric parallelism across every paragraph is the biggest tell.236- **Reference concrete artifacts** where possible: exact plan names, integration counts, file-size caps, review-quote fragments, specific product features. Not generic phrases like "clean interface" or "strong integrations."237- **Strip AI-tell transitions:** "Moreover," "Furthermore," "In conclusion," "Additionally," "It's worth noting that."238- **Strip near-synonym pairings:** "quick and efficient," "simple and straightforward," "fast, easy, and reliable."239- **Strip hollow imperatives:** "Empower your team to…" "Unlock the power of…"240241---242243## Rule 4 — Global "How we verified this" note244245At the top of every listicle (immediately before the first non-hero `<h2>` — usually `The 60-second TL;DR` or `The quick pick`), insert one global data-verification callout. Never per-tool.246247```html248<div style="background:#F9FAFB;border-left:4px solid #7f56d9;padding:14px 18px;margin:0 0 24px 0;border-radius:6px;font-family:inherit;">249<p style="margin:0;font-size:14px;color:#475467;font-family:inherit;"><strong>How we verified this:</strong> ratings, review counts, and prices sourced directly from each tool's own pricing / product pages and their public G2 review profile on <YYYY-MM-DD>. We rechecked every price and free-tier cap the morning of publish. Live listings may vary if a tool updates pricing after this date.</p>250</div>251```252253The date substitutes to build-time `today()`. **Never a hardcoded string** (per `feedback_no_hardcoded_dates.md`).254255---256257## Rule 5 — Body scaffolding (existing Minimal Structure conventions apply)258259Everything else about the body follows the standing blog rules:260261- **Minimal Structure mode** (`feedback_blog_minimal_structure_mode.md`): semantic HTML + fixed class catalog, no `@import` fonts, `font-family: inherit`, `<p style="font-size: inherit;">` on the lead paragraph.262- **FAQs** in `<section class="faq"><details><summary>Q</summary><div>A</div></details></section>` shape. Never external links inside FAQ answers.263- **Tables** wrapped in `<div class="table-wrap">`.264- **Mid-article violet CTA card** at 40-60% depth, before a meaningful H2 (per `feedback_blog_tailored_cta.md`). Copy is tailored per listicle — do not reuse the same CTA across all four.265- **No em-dashes anywhere** (per `feedback_no_em_dashes_hard_rule.md`). If a rewrite pipeline emits em-dashes, the preflight strips them, but treat that as a bug in the pipeline, not a working state.266- **No repeat internal links in body** (per `feedback_no_repeat_internal_links.md`).267268---269270## Rule 6 — Cover image271272Every listicle gets a v6 cover via `/cover-image`:273274- Headline lines split so the primary phrase gets an orange accent275- Illustration subject describes what the cream-card panel should show (a clipboard + form + chart for data collection, a quiz card interface for quiz funnels, etc.)276- Wordmark top-center, small (4% canvas height, 4.5% top pad) — this is the v6 default; no override needed277278After generation, upload the PNG to your CMS media library, set as `coverImg` on the blog, prepend `` to the body (powers the in-page hero), and set the `image` field on the Article node in the JSON-LD @graph.279280---281282## Rule 7 — Google Doc draft (approval gate before the CMS push)283284Per `feedback_drafts_always_in_gdoc.md`: never push to your CMS without a Google Doc draft first.285286**Doc structure is LOCKED by the shared standard at [`~/.claude/skills/_shared/doc_content_standard.md`](../_shared/doc_content_standard.md).**287288- Metadata table first (Title, Description, Meta title, Meta description, Primary keyword, Primary keyword volume (US), Secondary keywords, Slug, Recommended URL, CMS link). No other rows.289- Body: verbatim listicle copy in reading order — intro, `[H2]` per tool, `[H3]` sub-sections inside each tool (Overview / Pricing / Strengths / Weaknesses / Best for), comparison table as a real Docs table, FAQ + related reading at the end. Every heading tagged `[H1] / [H2] / [H3]` inline. Every image marker on its own line as `[Image: alt text]`.290- **No commentary, no rationale, no cover URL or hero-screenshot URLs, no G2 API-response JSON, no scoring rubric explanation.** Pricing / ratings / links land in body text or the comparison table verbatim.291- Convert the assembled spec via `~/.claude/skills/gdoc/build_gdoc.py` with `GDOC_AUTH_DIR={{GOOGLE_AUTH_DIR}}` ({{GOOGLE_ACCOUNT}} OAuth). The skill handles Geist typography and real Docs tables under the hood.292- When updating an existing listicle, regenerate the Doc in place via `--doc-id <existing_id>`.293- Share the Doc URL back to the user; wait for approval before CMS push294295---296297## Rule 8 — CMS push (draft state, `publishedAt: null`)298299After Google Doc approval, push to your CMS as a DRAFT. Never publish without a separate explicit approval (per `feedback_never_publish_without_approval.md`).300301Payload shape for `POST /api/blogs`:302303```json304{305 "data": {306 "title": "<meta title without | {{BRAND_NAME}}>",307 "description": "<longer blurb for blog cards>",308 "metaTitle": "<meta title 30-60 chars>",309 "metaDescription": "<meta desc 120-160 chars>",310 "keywords": "<comma-separated>",311 "author": "Harish Kumar",312 "authorProfile": "https://www.linkedin.com/in/harish-kumar2424/",313 "coverImg": <media_id>,314 "coverImgAlt": "<alt text>",315 "featured": false,316 "slug": "<slug>",317 "body": "<full HTML body>",318 "schema": [ ... 6 entities ... ],319 "publishedAt": null320 }321}322```323324**Duplicate check:** before POST, query `GET /api/blogs?filters[slug][$eq]=<slug>&fields[0]=slug&publicationState=preview`. Skip if it already exists (may be a re-run).325326---327328## Rule 9 — 5-entity (6-node) JSON-LD @graph329330Every listicle ships with the mandatory graph (per `feedback_seo_publish_checklist.md`):3313321. **Organization** — {{BRAND_NAME}}, referenced by `@id`3332. **WebPage** — `datePublished`, `dateModified`, `headline` (verbatim from title), `inLanguage`3343. **BreadcrumbList** — Home > Blog > <Listicle Name>3354. **Article** — `headline`, `description`, `datePublished`, `dateModified`, `author`, `publisher`, `mainEntityOfPage`, `image` (cover URL, set after cover upload)3365. **FAQPage** — every FAQ from the body, verbatim3376. **SoftwareApplication** — {{BRAND_NAME}} (the subject product), with `offers` array (Free at $0, Personal at $13) and `aggregateRating` (4.7/5, 11 reviews on G2)338339Dates resolve to `today()` at build time. **Never a hardcoded date string.**340341---342343## Rule 10 — Pre-publish preflight (mandatory, blocks publish)344345Before flipping `publishedAt`, run this checklist on the body + payload. Any FAIL blocks publish.346347- [ ] Meta title 30-60 chars348- [ ] Meta description 120-160 chars349- [ ] Slug matches the URL pattern350- [ ] No `<h1>` in body (title renders as H1 via the template)351- [ ] No em-dashes anywhere in the body (`—`)352- [ ] No banned placeholders: `TODO`, `FIXME`, `INSERT_`, `REPLACE_`, `PLACEHOLDER`, `[REUSE`, `[NEW TEMPLATE`353- [ ] All FAQs have no external `http(s)://` URLs (internal `{{SITE_HOST}}` OK)354- [ ] Schema has exactly 6 entities: Organization + WebPage + BreadcrumbList + Article (with image) + FAQPage + SoftwareApplication (with AggregateRating)355- [ ] Global "How we verified this" note is present at the top356- [ ] Cover image is uploaded, `coverImg` is set, and body prepends `` for the in-page hero357- [ ] Every tool section has a hero screenshot, Strengths bullets, Weaknesses bullets, Pricing bullets, and a G2 rating line (real link or "Not listed on G2" fallback)358- [ ] Every claim about competitors traces to `competitor-research.md` or a Firecrawl fetch documented in `research.json`359- [ ] Every claim about {{BRAND_NAME}} traces to `brand-facts.md`360- [ ] **MANDATORY hygiene preflight — call `run_preflight()` from `~/.claude/skills/_shared/preflight.py` inside the build/push code path BEFORE building the CMS payload, and persist the result on disk so the source-of-truth stays in sync.** Wired into `scripts/build_listicle_bodies.py` 2026-07-27.361 ```python362 import sys; sys.path.insert(0, str(Path.home() / ".claude/skills/_shared"))363 from preflight import run_preflight, summarize_report364 body, report = run_preflight(body)365 ```366 Runs three scrubbers in order: `strip_trailing_periods` (no periods on headings, badges, CTAs), `fix_button_hover_color` (button `:hover` keeps base color with `!important` — never inherits to browser-default black), `fix_markdown_leakage` (catches `**bold**` markers the md→html converter missed, unwraps `<em>` blocks that swallowed paragraph body as italic, merges numbered sub-items into the canonical `<p><strong>Label.</strong> Body</p>` shape, rebuilds malformed `<strong>` tags). Body paragraphs and FAQ answers keep their periods; headings + `__title` / `__badge` / `__eyebrow` / `__pill` / `__label` / `__cta` classes do not. **Rationale:** `/tally-vs-typeform/` shipped 2026-07-26 with heading periods; `Sign Up Free` button shipped with hover text dropping to black; reviews 1855-1858 shipped with markdown leaks — every one because bespoke adapter push scripts skipped the checks. Never again.367- [ ] **Audit-scan preflight (BLOCKS publish).** Run `python3 ~/.claude/skills/_shared/audit_scan.py <body.html>` on the assembled listicle body. Exit 1 (critical) = do not push, fix and re-run. Exit 2 (high) = treat as blocker unless operator overrides. Catches: {{BRAND_NAME}} HIPAA/SOC 2/ISO overclaim, {{BRAND_NAME}} stale caps, "(verify)" placeholder leaks, recycled offline-field-teams Strengths block signatures via Razorpay/HIPAA-roadmap phrasing, hardcoded date strings, em-dashes, spaced-comma typos. Rationale: `best-quiz-funnel-software` and `best-poll-maker` shipped 2026-07-15 with literal `(verify)` in copy, cross-tool plan-name contamination, and recycled Strengths blocks; both had to be unpublished 24 hours later.368369---370371## Rule 11 — Publish (only on explicit approval)372373Per `feedback_never_publish_without_approval.md`: never flip `publishedAt` without explicit user approval on the current blog. Prior approvals do not carry forward.374375When approved:3763771. Set `publishedAt` to `datetime.now(timezone.utc).isoformat()` (Z suffix, no microseconds)3782. Confirm the `image` field is now populated on the Article schema node3793. PUT the payload to `PUT /api/blogs/<id>`3804. Poll `{{SITE_URL}}/blog/<slug>/` for 200. Expect a delay of 5-15 min for the static rebuild. If it stays 404 past 15 min, flag it and ask the user whether to trigger the Netlify/Vercel build hook.381382---383384## Rule 12 — Cluster Master Tracker update385386After the CMS draft is created, append a row to the tracker.387388- Sheet ID: `1yXUhAEZKe-TZsBNK09nCJOlRq4SD1glm10w576aN3as`389- Tab name: `Cluster Master Tracker` (NOT `Master Tracker` — that's a 400)390- Header range: `A1:AB1` (28 cols, verified 2026-07-09)391- Column semantics per `feedback_master_tracker_locked_schema.md`392393On draft push: Status = `Ready`, Live URL = CMS admin URL (`{{CMS_ADMIN_URL}}/api::blog.blog/<id>`), Notes = draft summary.394395On publish approval: flip Status to `Published`, Live Date + Publish date to `today()`, Live URL to `{{SITE_URL}}/blog/<slug>/`, refresh Notes to reflect ship state.396397---398399## Rule 13 — Reference implementation400401The 4 listicles shipped 2026-07-10 are the reference. Working scripts live under `~/.claude/skills/listicle/scripts/`:402403- `build_listicle_bodies.py` — markdown → Minimal Structure body HTML, injects tailored CTA per slug, strips em-dashes, wraps lead in `font-size: inherit`404- `embed_listicle_shots.py` — Firecrawl viewport screenshots → your CMS media library → inject `<img>` after each tool H2405- `rewrite_all_sections.py` — orchestrator for the deep-section rewrite (paragraphs + strengths + weaknesses + pricing + G2)406- `gen_covers_top_center.py` — helper wrapper for /cover-image v6 (top-center small wordmark); can be inlined since v6 is now the default407- `publish_listicles.py` — preflight + schema build + PUT + tracker update408409Copy these as starting points; do not treat them as immutable.410411**Reference entries:** 1830 (best-data-collection-software), 1831 (best-quiz-funnel-software), 1832 (best-questionnaire-software), 1833 (best-poll-maker).412413---414415## Rule 14 — Things to never do416417- **Never invent competitor pricing.** Always source from `competitor-research.md` or a Firecrawl fetch. Anything ambiguous asks the user.418- **Never fabricate G2 ratings.** If a tool has no G2 profile, use the "Not listed on G2" line. Never invent a number.419- **Never claim {{BRAND_NAME}} features that aren't shipped.** Cross-check `brand-facts.md`. {{BRAND_NAME}} is NOT HIPAA-certified, NOT SOC 2, NOT ISO 27001 — do not soften these into "compliant with major frameworks."420- **Never use em-dashes** anywhere in the body, FAQ answers, headlines, or alt text.421- **Never leave trailing full stops on headings, badges, CTAs, feature titles, or short UI copy.** Headings like "Free.", "Easy.", "Fast." or "{{BRAND_NAME}} vs X: the honest comparison." all drop the period. Body paragraphs and FAQ answers keep periods (they are full sentences). Run `~/.claude/skills/_shared/strip_trailing_periods.py` before every push.422- **Never hardcode dates** in generated components. All dates resolve to `today()` at build time.423- **Never publish without explicit user approval** on the current blog. Prior approvals do not carry forward.424- **Never skip the Google Doc approval step.** CMS drafts get pushed AFTER the user has approved the Doc.425- **Never use the wrong OAuth token.** Sheets/GSC use the `.env` refresh-token OAuth; Docs/Drive use `{{GOOGLE_AUTH_DIR}}/token.json`. Both under `{{GOOGLE_ACCOUNT}}`. Never `{{GOOGLE_ACCOUNT}}`.426- **Never write "Data verified on <date>" per tool** anywhere in the body. That goes in one global "How we verified this" callout at the top.427428429## No self-referential media captions (HARD RULE, added 2026-08-09)430431Never add authenticity remarks under or near screenshots, GIFs, or videos: "Real {{BRAND_NAME}} UI, captured in-app", "actual product screenshot", "real UI, not a mockup", "captured from the live app", or any similar meta-commentary about the asset being real. These read as AI-generated filler and were ordered removed site-wide. The asset speaks for itself. If a media element genuinely needs a caption, the caption states only WHAT the user is looking at (e.g. "Form builder, Design tab"), never that it is real, actual, or captured. This applies to every page type: product, landing, alternative, blog, listicle, review, template.432433434## Inspiration pages are design-only, never fact sources (GLOBAL HARD RULE, added 2026-08-10)435436Root cause: the Aug 2026 factual audit found 4 alternative pages shipped with the inspiration page's facts (Wufoo's pricing tiers, ownership, founding year) left in place for different competitors. This must never recur.437438- An internal inspiration/reference page may be reused ONLY for: page structure, section structure, layout, design patterns, visual treatment, formatting, component patterns, UX approach.439- NEVER carry over content, claims, facts, statistics, pricing, plan names, limits, ownership, founding dates, quotes, or any information from the inspiration page into a new page.440- Research comes FIRST on every new page: (1) research the subject independently, (2) identify current correct information, (3) verify every factual claim against first-party/authoritative sources (official pricing page, docs, company newsroom), (4) write original content from that research.441- Never assume something is correct because it appears on an existing {{BRAND_NAME}} page. Never fill gaps with guesses. If a claim cannot be verified, flag it explicitly instead of presenting it as fact.442- Cloned template blocks that carry competitor-specific facts (pricing tables, "why teams leave" cards, honest-comparison cards, free-plan tables) must be rebuilt from fresh research for every new subject, not find-and-replaced.
Run npx skillmds@latest add harsh719/listicle in your terminal (requires Node.js), paste this page's agent-chat prompt into Claude, Cursor, or any MCP-connected agent, or download the SKILL.md file and copy it into your agent's skills directory.
Build a {{BRAND_NAME}} listicle / comparison blog end-to-end. Research live G2 ratings + pricing per tool via Firecrawl, capture hero screenshots, write deep per-tool sections (paragraphs + strengths / weaknesses / pricing bullets + clickable G2 link), generate a v6 cover image, draft a Google Doc for approval, push to your CMS as a draft with 5-entity JSON-LD @graph, run SEO preflight, publish only on explicit approval, update the Cluster Master Tracker. Trigger with `/listicle` or when the user asks to "build a listicle", "write a best-of-X blog", or "compare N tools" for {{BRAND_NAME}}. It is listed under Marketing & Growth on SkillMD.
This skill has not completed SkillMD's automated safety review yet. SkillMD never runs a skill's scripts for you; review the SKILL.md before installing.
This skill is tagged as working with Claude Code, Claude.ai, OpenAI Codex. SKILL.md is an open format, so most agents that read a skills directory can load it too.
Yes. Installing skills from SkillMD is free, and the skill stays under its author's original license.
harsh719 (@harsh719) published this skill. Their other Agent Skills are listed on their SkillMD profile.