# Blog

> <!-- SETUP:BEGIN -->

- Skill: `harsh719/blog` (Agent Skill)
- Install (CLI): `npx skillmds@latest add harsh719/blog`
- Raw SKILL.md: https://api.skillmd.com/api/skills/harsh719/blog/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: harsh719 (https://skillmd.com/u/harsh719)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/harsh719/blog

---

<!-- SETUP:BEGIN -->
## Before you run this skill

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:**

1. Run `python3 brandkit.py`. It prints the config source and any placeholder
   that is still unset.
2. If it says `configured: False`, copy `brand.config.example.json` to
   `brand.config.json`.
3. Ask the operator for each value under `missing`, then write them in. Do not
   guess a brand name, a domain, or a colour.
4. 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.

<!-- SETUP:END -->

<!-- CMS:BEGIN -->
## 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.

```python
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.

<!-- CMS:END -->

# {{BRAND_NAME}} Blog Builder (how-to / guide / tutorial)

Ships a {{BRAND_NAME}} how-to blog end-to-end — informational articles, step-by-step guides, tutorials — with mandatory Phase 0 SERP + LLM ranking-factor recon **before** any content is written. Body ships in Minimal Structure mode (no scoped CSS, no landing-page prefixed classes). Optional real in-app screenshots captured via authenticated Playwright and embedded under each step H3.

**Reference implementation:** `/blog/how-to-create-a-consent-form/` (reference entry). Scratchpad artifacts + captured screenshots at `<repo>/scratchpad/{{BRAND_NAME}}-screenshots/` and `<repo>/scratchpad/blog_1789_*.html`.

**Related skills** (do NOT overlap with this one):

- `/listicle` — best-of-N comparison blogs (7+ tool tables, G2 ratings, hero shots). Use that instead of `/blog` when the article is a ranked comparison.
- `/review-page` — competitor review blogs (hands-on 30-day-style review of a single tool). Use that instead when the article is a review.
- This skill (`/blog`) — how-to guides, informational articles, step-by-step tutorials. Everything else that's a blog but not a listicle or review.

---

## Rule 0 — Inputs (ask one at a time, echo back, confirm)

1. **Primary keyword** — target search phrase (US). Example: `how to create a consent form`, `how to build a job application form`, `what is form validation`.
2. **Slug** — kebab-case URL path under `/blog/`. Example: `how-to-create-a-consent-form`.
3. **Blog title** — exact title as shown on page (60-70 chars). Example: `How to Create a Consent Form (5 Steps + Templates)`.
4. **Blog angle** — one of: `step-by-step how-to` (numbered steps), `informational guide` (explainer with subsections), `comparison-lite` (2-3 options compared, no ranked listicle), `checklist / playbook` (bulleted actions to run).
5. **In-app screenshots** — yes / no. If yes, we log into `{{APP_HOST}}` via Playwright and capture one screenshot per step showing the actual {{BRAND_NAME}} UI. Default: yes for step-by-step how-tos, no for informational.
6. **Companion {{BRAND_NAME}} feature to demonstrate** — the specific feature the blog builds toward. Example: `consent form with e-signature`, `AI-generated survey`, `PDF-to-form workflow`. This informs which template we open in the app when capturing screenshots.
7. **Regulated-industry framing** (only if applicable) — one of:
   - `explicit-not-hipaa-with-routing` — {{BRAND_NAME}} is not HIPAA-certified, route PHI use to Jotform Gold / Cognito Enterprise (recommended for medical / consent / clinical topics)
   - `compact-caveat` — one-sentence non-HIPAA note only
   - `not-applicable` — topic is not health/consent adjacent, skip HIPAA entirely

After all seven, echo back in one block and wait for explicit confirmation before Phase 0.

---

## Rule 1 — 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:**

1. **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.
2. **Firecrawl top 5 commercial competitors** (skip academic `.edu` unless the topic is IRB / research-vertical). Extract H2/H3 tree, word count, entity coverage, EEAT signals, distinctive sections our default template misses.
3. **LLM citation probe** via OpenAI `gpt-4o-search-preview` × 4 query variants (informational / how-to / specific-use-case / comparison). Parse `annotations[].url_citation` for cited domains + track {{BRAND_NAME}} body mentions.
4. **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 section plan, meta title + description, HIPAA framing decision.

**Wait for explicit user approval of the synthesis before proceeding to the content build.** The user can adjust scope, personas, step count, screenshot list, positioning at this stage.

**Never skip.** Every content skill that shipped without Phase 0 in 2026-07 landed with 0/4 LLM citations and wrong-intent-bucket SERP positioning (documented in the 11-page audit 2026-07-31).

---

## Rule 2 — Minimal Structure blog format (LOCKED)

The single hardest lesson from blog 1789's first version: **never ship a blog with scoped `<style>` blocks or landing-page-style prefixed classes**. Blogs use Minimal Structure mode per `feedback_blog_minimal_structure_mode.md` — semantic HTML plus a small fixed class catalog styled by the site stylesheet.

**Required elements every blog ships:**

| Element | Pattern |
|---|---|
| Lead paragraph | `<p style="font-size: inherit;">Lead sentence.</p>` (per `feedback_blog_lead_paragraph_inherit.md`) |
| Cover image | `![alt](cover-url)` markdown at the very top of body, ABOVE the lead paragraph (per `feedback_blog_cover_render_pattern.md`) |
| Body paragraphs | `<p>Body copy.</p>` |
| Headings | `<h2>`, `<h3>` (NO `<h1>` — the CMS renders the title) |
| Bulleted lists | `<ul><li><strong>Label.</strong> Body.</li></ul>` |
| Numbered lists | `<ol><li>Step.</li></ol>` (or use H3 numbered headings like "Step 1. Title" for main workflow steps) |
| Tables | `<div class="table-wrap"><table>…</table></div>` (per `feedback_blog_body_width_mobile_v3.md` — mobile scroll wrap) |
| Callouts / quick answer | `<blockquote><p><strong>Quick answer.</strong> …</p></blockquote>` |
| Mid-article violet CTA | `<div class="{{CSS_PREFIX}}-blog-cta"><p><strong>CTA hook.</strong> Trust line.</p><p><a class="{{CSS_PREFIX}}-blog-cta__btn" href="…">CTA button</a></p><p class="{{CSS_PREFIX}}-blog-cta__meta">Free forever plan · No credit card · Setup in 2 minutes</p></div>` (per `feedback_blog_tailored_cta.md`, ~40-60% depth) |
| FAQ block | `<section class="faq"><details><summary>Question?</summary><p>Answer.</p></details>…</section>` (per `feedback_blog_faq_section_wrapper.md` — wrapper is required for accordion CSS) |
| Related reading | `<h2>Related reading</h2><ul><li><a><strong>Title</strong></a> - description.</li></ul>` (simple list, or the 2-col grid pattern per `feedback_blog_related_v2_canonical.md` if we want the richer treatment) |
| Internal links | Each internal URL linked AT MOST once (per `feedback_no_repeat_internal_links.md`). Related-grid at the bottom is the exception. |
| FAQ answers | No external `http(s)://` URLs (per `feedback_no_external_links_in_faqs.md`). Internal `{{SITE_HOST}}` links are fine. |
| No em-dashes | ZERO. Not in body, not in FAQ answers, not in table cells. Use commas, colons, semicolons. |
| No hardcoded dates | Every "as of X" or "updated Y" resolves to `today()` at build time OR uses "at time of writing" phrasing. |

**Explicitly banned in blog bodies** (all landed in blog 1789's first version and had to be stripped):

- `<style>` blocks (any number, for any reason)
- `{{CSS_PREFIX}}-cmp-*` prefixed CSS classes (that's the landing-page convention)
- `<section class="{{CSS_PREFIX}}-cmp-…">` wrappers around content blocks
- `!important` declarations
- `@import` font URLs
- Section-level padding overrides
- Font family declarations (site stylesheet handles this)

**Reference:** open `HTML component/consent-form-generator` (page 898) for the LANDING-page-style pattern. Open blog 1789's live body (`GET /api/blogs/1789?publicationState=preview` → `body` field) for the BLOG-style pattern this skill produces. They are visually and structurally distinct on purpose.

---

## Rule 3 — In-app Playwright screenshots (optional but recommended for how-tos)

If Rule 0 input 5 = yes, capture one screenshot per numbered step showing the actual {{BRAND_NAME}} UI. This is the most-underrated LLM-citation signal — real product screenshots inside a how-to blog signal first-hand experience, which LLMs and Google both weight.

### 3a — Login flow (Playwright)

Credentials load from `.env` at project root: `BRAND_APP_EMAIL` and `BRAND_APP_PASSWORD` ({{GOOGLE_ACCOUNT}} account).

```python
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    ctx = browser.new_context(viewport={'width': 1440, 'height': 900})
    page = ctx.new_page()

    page.goto('{{SIGNUP_URL}}', wait_until='domcontentloaded', timeout=45000)
    page.wait_for_timeout(3000)
    page.locator('#user_email').fill(EMAIL)
    page.locator('#user_password').fill(PASS)
    page.locator('input[type="submit"][name="commit"]').click()  # exact selector; button[type="submit"] misses because it's <input>
    page.wait_for_url('**/forms**', timeout=30000)
    page.wait_for_load_state('networkidle')
```

### 3b — App URL reference (all discovered on the "Client Consent Form" template, form id 47463, 2026-08-04)

- `/forms` — dashboard (form cards)
- `/forms/create` — modal: "How do you want to create a new Form?" with 6 options (Start with AI, Start From Scratch, Use a Template, Import Form, PDF to {{BRAND_NAME}} Form, Backend Form). **Ideal for "choose type of form" step.**
- `/forms/{ID}/builder` — Build tab: form editor with left canvas + right Design/Advanced panel. Bottom nav shows page tabs (Welcome / Page 1 / Page 2 / Thank you). **Ideal for "write form body" and "add signature field" steps** (click Page N to see fields).
- `/forms/{ID}/pdf_editor` — PDF Editor tab
- `/forms/{ID}/embed` — Embed tab
- `/forms/{ID}/share` — Share tab
- `/forms/{ID}/submissions` — Results tab (Submissions / Analytics / Summary). **Ideal for "audit-ready records" and "timestamp / secure storage" steps** — shows search, filter, columns, Export as CSV.
- `/forms/{ID}/autoresponder` — Automate tab (default sub-tab). Sub-tabs: Autoresponder / Email Notifications / Integrations / AI Workflows / Webhooks. **Ideal for "send PDF copy to signer" step** — click "Add Autoresponder" to reveal the config modal with Email Body + Email Attachments.
- `/forms/{ID}/email_notifications` — Email Notifications sub-tab direct
- **Gear icon in top nav opens a Template Settings MODAL, not a URL. Not useful for standard how-to steps.**

### 3c — Wait for SPA content before screenshotting

The builder is a heavy Vue/React SPA — a bare `page.wait_for_timeout(3000)` fires before content renders and produces 5-6KB blank white PNGs. Fix:

```python
page.goto(f'{{APP_URL}}/forms/{ID}/builder', wait_until='networkidle', timeout=45000)
page.wait_for_timeout(8000)  # extra buffer for SPA hydration
page.screenshot(path=out_path)
```

Any PNG under 30KB is a load-failure — retry with longer wait or click a specific tab to trigger content render.

### 3d — Screenshot upload + embed

Upload each to your CMS media library via `/api/upload` multipart POST (see the CMS adapter in `cms/adapters.py` for the exact multipart shape).

Embed under each step's H3 heading as:

```html
<p><img src="{cover-url}" alt="{descriptive alt}" loading="lazy" style="width:100%;max-width:900px;height:auto;border-radius:8px;border:1px solid #e5e7eb;margin:16px 0;" /></p>
```

Position: after the step's H3 + the immediate intro `<p>`, BEFORE any bullet list of details. This matches the pattern shipped on blog 1789.

---

## Rule 4 — Cover image (mandatory)

Every blog ships with a v6 cover.

1. Invoke `/cover-image` skill (or call `~/.claude/skills/cover-image/build.py` directly with the four inputs: `--slug`, `--lines`, `--orange`, `--illustration`).
2. Save PNG locally to `<repo>/scratchpad/blog-covers/<slug>.png`.
3. Upload to your CMS media library via `/api/upload` → capture the returned `id` and `url`.
4. Set `blog.coverImg = <media_id>` and `blog.coverImgAlt = "<descriptive alt>"` on the blog record.
5. **Prepend `![alt](cover-url)\n\n` to body** so the in-page hero renders (per `feedback_blog_cover_render_pattern.md`). This is the ONE place where markdown mixes with HTML in the body — required, load-bearing.

The cover pattern is the SAME for every content type at {{BRAND_NAME}} (v6, wordmark top-center, illustration left / headline right, deep indigo bg). No blog-specific variant.

---

## Rule 5 — Google Doc draft (approval gate, optional but recommended)

After the body is drafted and screenshots are embedded but BEFORE CMS push, generate a Google Doc for content review.

- Use `/gdoc` with `GDOC_AUTH_DIR={{GOOGLE_AUTH_DIR}}` ({{GOOGLE_ACCOUNT}} OAuth per `the Google auth note in the README.md`).
- Doc lands in "{{BRAND_NAME}} SEO Audits" Drive folder.
- **Doc structure follows the shared standard at [`~/.claude/skills/_shared/doc_content_standard.md`](../_shared/doc_content_standard.md).** Metadata table first (Title, Description, Meta title, Meta description, Primary keyword, Primary keyword volume (US), Secondary keywords, Slug, Recommended URL, CMS link) → verbatim body copy with explicit `[H1] / [H2] / [H3]` heading markers and `[Image: ...]` placement markers. **No commentary, no rationale, no `**Why:**` lines, no placement notes, no schema blocks, no cover URL or media ids.**
- When updating an existing page (blog PATCH), regenerate the Doc in place via `--doc-id <existing_id>`.
- Share URL back to user, wait for content approval before the CMS push PATCH.

Skip if the user says "just push it" — this is a friction-reduction option, not a hard gate. Publish approval on the LIVE blog is still a hard gate (Rule 9).

---

## Rule 6 — CMS push (POST /api/blogs, draft state)

After preflight (Rule 8) passes:

1. **Payload shape:**

```python
payload = {
    "data": {
        "slug": SLUG,
        "title": TITLE,                            # H1 on page
        "description": DESCRIPTION,                # 2-3 sentence blurb (blog card + og description)
        "metaTitle": META_TITLE,                   # 30-70 chars, ends with "| {{BRAND_NAME}}" (optional but recommended)
        "metaDescription": META_DESCRIPTION,       # 120-160 chars
        "keywords": ",".join(KEYWORDS),            # comma-separated string, NOT a list
        "author": "Harish Kumar",                  # or "Harsh Shah" for personal-voice how-tos
        "authorProfile": "https://linkedin.com/in/harshshahseo",
        "coverImg": COVER_MEDIA_ID,                # int, from Rule 4 upload
        "coverImgAlt": COVER_ALT,                  # string
        "featured": False,
        "body": BODY_HTML,                         # ALL Minimal Structure HTML from Rule 2, ![alt](cover-url) prepended
        "schema": JSONLD_GRAPH_LIST,               # Rule 7 — LIST of {id, type} entries, NOT a nested dict
        "publishedAt": None,                       # HARD RULE — never publish on POST
    }
}
```

2. **`body` field** — Minimal Structure HTML, with the cover markdown image as the FIRST line, then the lead `<p style="font-size: inherit;">`, then everything else.

3. **`schema` field on blogs (Strapi only)** — it stores this as a LIST of `{id, type}` entries in a component-repeatable, NOT a single dict. Each entry's `type` is one JSON-LD entity. See the CMS adapter in `cms/adapters.py` for the exact shape. **The blog page template does NOT render the `schema` field to `<script type="application/ld+json">` today** — this is a known Nuxt template bug documented in this session's earlier schema investigation. Ship the schema anyway (the CMS stores it correctly, and any future template fix picks it up).

4. **Duplicate slug check first** — GET `/api/blogs?filters[slug][$eq]=<slug>&publicationState=preview`. If exists, abort with report unless the user explicitly said "PATCH the existing blog."

5. **POST** — expect 200 with the new blog id.

6. **Verify** via GET with `publishedAt: null` confirmed. Report the CMS admin URL + preview URL: `{{SITE_URL}}/preview/?token=<preview_token>&type=blog` (your CMS returns the preview token in the response; if not, generate via the CMS admin UI).

Never publish on POST. Rule 9 is the only place `publishedAt` gets set to a timestamp.

---

## Rule 7 — 6-entity JSON-LD @graph (mandatory even though Nuxt doesn't render it yet)

Every blog's `schema` list must contain these entities. Nuxt's blog template ignores the `schema` field today (documented bug), but the CMS stores it and any future template fix picks it up. Do NOT skip.

1. **Organization** — {{BRAND_NAME}}, referenced by `@id: {{SITE_URL}}/#organization`
2. **WebPage** — url, name, description, `datePublished` = today, `dateModified` = today, `inLanguage: en-US`, isPartOf → Organization, breadcrumb → BreadcrumbList
3. **BreadcrumbList** — Home → Blog → post title
4. **Article** — headline, description, `image` = cover URL, `datePublished`, `dateModified`, `author` (Person with url), `publisher` = Organization, `mainEntityOfPage` = WebPage
5. **FAQPage** — every FAQ from the body's `<section class="faq">` block, verbatim as `{"@type": "Question", "name": q, "acceptedAnswer": {"@type": "Answer", "text": a}}`
6. **SoftwareApplication** — {{BRAND_NAME}} product entity: name, url, `applicationCategory: "BusinessApplication"`, `operatingSystem: "Web, iOS, Android"`, `offers` (Free $0 + Personal $13), `aggregateRating` (4.7/5, 11 reviews on G2)

Dates resolve to today at build time. Never a hardcoded string.

---

## Rule 8 — SEO preflight (blocks publish, mandatory)

Auto-run before the publish step. Any FAIL blocks the flip to live.

- [ ] Meta title 30-70 chars (blogs allow slightly longer than product pages)
- [ ] Meta description 120-160 chars
- [ ] Slug matches URL path (`/blog/<slug>/`)
- [ ] Body starts with `![alt](cover-url)` markdown image (cover hero)
- [ ] Body has `<p style="font-size: inherit;">` lead paragraph immediately after cover
- [ ] Zero `<style>` blocks in body
- [ ] Zero `{{CSS_PREFIX}}-cmp-*` prefixed classes in body
- [ ] Zero `<h1>` tags in body (title renders as H1)
- [ ] Zero em-dashes anywhere
- [ ] Zero banned placeholders: `TODO`, `FIXME`, `INSERT_`, `REPLACE_`, `PLACEHOLDER`
- [ ] Every internal URL linked at most once outside the related-grid
- [ ] Zero external URLs inside `<section class="faq">` answers
- [ ] FAQ block present with at least 5 `<details>` items
- [ ] `<section class="faq">` wrapper present around the FAQ block
- [ ] Every table wrapped in `<div class="table-wrap">`
- [ ] `blog.coverImg` set, `blog.coverImgAlt` set
- [ ] `blog.schema` has 6 entities per Rule 7 (Organization / WebPage / BreadcrumbList / Article / FAQPage / SoftwareApplication)
- [ ] `publishedAt: null` (still a draft)
- [ ] `run_preflight()` from `~/.claude/skills/_shared/preflight.py` on the assembled body — returns clean (0 fixes)
- [ ] `audit_scan.py` on the assembled body — exit 0

---

## Rule 9 — Publish (only on explicit approval)

Per `feedback_never_publish_without_approval.md`: never flip `publishedAt` without explicit user approval on THIS specific blog. Prior approvals do not carry forward.

When approved:

1. Set `publishedAt` to `datetime.now(timezone.utc).replace(microsecond=0).isoformat() + 'Z'`.
2. PUT to `/api/blogs/<id>`.
3. Poll `{{SITE_URL}}/blog/<slug>/` for 200. Nuxt static rebuild takes ~5-15 minutes; 404 in that window is expected.
4. Log to the Cluster Master Tracker `Published Content Tracker` tab if the source row exists.

---

## Rule 10 — Reference implementation

**Canonical reference:** `/blog/how-to-create-a-consent-form/` (reference entry).

- Body: `GET /api/blogs/1789?publicationState=preview` → `body` field. 18,718 chars, 5 embedded step screenshots, cover markdown image at top, `<section class="faq">` with 8 `<details>` items, mid-article `<div class="{{CSS_PREFIX}}-blog-cta">` after the templates section, 1 semantic `<table>` in `<div class="table-wrap">`.
- Cover: media id 4754, generated via `/cover-image` with `--lines "How to Create a" "Consent Form" "in 5 Steps"` and `--orange Consent Form 5`.
- Screenshots: media ids 4749-4753, captured from {{BRAND_NAME}} template form id 47463 ("Client Consent Form") via authenticated Playwright.

When in doubt about a specific pattern (mid-CTA structure, FAQ shape, table wrap, lead paragraph), fetch blog 1789's body and copy the exact HTML.

---

## Rule 11 — Never do

- Never publish without explicit approval on THIS specific blog.
- Never ship a blog with `<style>` blocks or `{{CSS_PREFIX}}-cmp-*` prefixed classes — that's landing-page HTML, not Minimal Structure. Blog 1789's original body was 47KB of scoped CSS; the rewrite dropped it to 17KB of semantic HTML.
- Never use em-dashes anywhere in body.
- Never hardcode dates in generated body copy.
- Never invent {{BRAND_NAME}} features. HIPAA / SOC 2 / ISO 27001 are all NO. Never claim "on the roadmap" for any of these.
- Never let a step screenshot fire before the SPA hydrates — 5KB PNGs = load failure. Wait longer.
- Never write the cover image URL to `body` without also setting `blog.coverImg` and `blog.coverImgAlt` — the cover serves two purposes (og:image + in-page hero) and both need the media set.
- Never skip Phase 0 SERP + LLM recon. Every blog that shipped without it in 2026-07 was invisible to LLMs (documented in the 11-page audit 2026-07-31).
- Never use `body` field name on `micro-components.raw-html` components. Blogs don't have components (they use the `body` field which IS correct on blogs), but if you're building alt-pages or product-pages inside a blog-adjacent flow, the field there is `markup`.
- Never let the schema field skip the FAQPage entity. The 6-entity graph is 6 or nothing. Even if Nuxt doesn't render it today, the CMS stores it and Google's Rich Results Test can validate the record.


## 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.

