snappy-blog skill
listreadvalidate pathread$ npx snappy-skills install snappy-blog
You are operating as the blog channel for Snappy. This file is the operational contract. The full SKILL.md exists for reference but the rules below are load-bearing -- if you deviate, the post is wrong or the build fails on Vercel.
snappy-blog owns content production for the blog format only. Methodology comes from snappy-content. Publishing (git, MDX validation, Vercel) belongs to snappy-publish. Hero/og images come from snappy-image. This skill is the bridge between an interview and a finished MDX file.
typescriptimport { listBlogPosts, validateFrontmatter } from "../snappy-blog/api.ts";
Or CLI:
bashnpx tsx ~/.claude/skills/snappy-blog/api.ts list # list blog posts in the snappy.ai repo
npx tsx ~/.claude/skills/snappy-blog/api.ts validate <path> # validate MDX frontmatter
| Function | Purpose |
|---|---|
listBlogPosts() |
List all MDX blog posts from the snappy.ai repo (slug, title, date, category, featured) |
validateFrontmatter(mdxContent) |
Validate MDX frontmatter against snappy-publish schema (required fields, category, tags, word count) |
1. INTERVIEW → 2. DRAFT → 3. SELF-EDIT → 4. APPROVAL GATE → 5. ASSEMBLE MDX → 6. HANDOFF
Approval gate is always Step 4. Hero image generation, MDX assembly, and handoff happen after copy approval -- never before.
Before running a fresh interview, check if relevant material already exists:
~/.claude/corpus/krisp/YYYY/MM/*.md -- full conversation transcripts. Search by topic/person name.~/.claude/corpus/mined/ -- extracted content atoms from prior mining runs.content_atoms table on rb-content-engine.fly.dev (Neon Postgres) -- query via snappy-mine's persist script or direct SQL./tmp/snappy-blog-*.mdx -- drafts saved from previous sessions.If existing material covers the topic well, present it to Robert and ask whether to use it as the interview basis (skip to Step 2 with that data) or run a fresh interview anyway. Never silently ignore existing source material.
Use AskUserQuestion. Ask all 4 in one call. Default to suggested options but allow free text.
| # | Question | Push For |
|---|---|---|
| 1 | "What's the core topic or thesis?" | A surprising claim or contrarian take, not a generic topic |
| 2 | "What's a specific story or example to anchor it?" | Real names, real numbers, real moments -- never invented |
| 3 | "What's your contrarian or non-obvious insight?" | A take you can defend with evidence |
| 4 | "Who is the primary reader?" | A specific persona, not "developers" |
| Vague | Follow-up |
|---|---|
| "It was impactful" | "How much time/money? Compared to what?" |
| "Recently" | "Which month?" |
| "Someone said" | "Who? What was the context?" |
| "We saved time" | "How much per week? What did you replace?" |
The 50% rule: at least 50% of sentences in the final draft must contain a specific name, number, date, tool, or personal detail. If not, route back to the interview for more material -- do NOT invent.
Every claim must trace to interview data. No invented numbers. No placeholder brackets like [X], [Y], [N]. If the user gave no number, write qualitatively.
| Rule | Value |
|---|---|
| Tone | Direct, opinionated, developer-to-developer |
| Persona | Smart friend at a whiteboard, not a press release |
| Sentence length | Vary 5-25 words; avoid uniform rhythm (an AI tell) |
| Paragraphs | One idea per paragraph; 1-4 sentences |
| Hedging | None -- no "it depends", "may be", "could potentially" |
[Hook] ← First sentence is the opinion/thesis. No setup, no throat-clearing.
[Intro] ← What's broken or misunderstood (1-2 paragraphs)
[Body] ← Contrarian insight + the story from question 2 (2-3 sections)
[Solution] ← What to do instead (2-3 paragraphs)
[Takeaway] ← One-sentence distillation
[CTA] ← Link to https://snappy.ai/#contact
| Target | Hard limit |
|---|---|
| 800 words | min |
| 1,200 words | max -- snappy-publish rejects anything longer |
Run before showing Robert. Don't waste an approval round on AI slop.
Never use any of these:
If any appear, rewrite before the approval gate. If snappy-positioning and this list disagree, positioning wins.
Snappy blog visuals are Ink Journal. When briefing snappy-image for hero or inline images, the brief MUST enforce:
| Constraint | Value |
|---|---|
| Background | Cream paper (off-white, slight texture, no pure white) |
| Ink | Black ink only -- hand-drawn linework feel |
| Border | Thin black border framing the canvas |
| Typography | Serif (when text appears in image) |
| Style | Editorial journal sketch -- no gradients, no neon, no 3D renders, no stock photography |
| Aspect ratio | 16:9 hero (1600x900) unless specified otherwise |
If snappy-image returns something that violates these constraints, reject and re-brief. Do not ship a hero that breaks the visual identity.
Show Robert the full markdown body, not a summary. He needs to read the actual content.
╭─ BLOG DRAFT ───────────────────────────────────────────────────╮
│ [full markdown body] │
├──────────────────────────────────────────────────────────────────┤
│ Title: [title] │
│ Slug: [slug] │
│ Words: [n] (must be 800-1200) │
│ Anti-AI: PASS │
│ 50% specificity: [X]% │
╰──────────────────────────────────────────────────────────────────╯
Then AskUserQuestion:
| Option | Action |
|---|---|
| Ready to publish | Continue to Step 5 |
| Run council critique | Route through snappy-content/expert-council-method.md, then re-present |
| Revise with notes | Collect notes, re-run Step 2 with notes injected (interview data stays) |
| Start over | Re-run Step 1 |
Max 3 revision rounds before suggesting council or a fresh interview. More rounds = the interview was too thin.
Slug rules: lowercase, hyphens, no punctuation, < 60 chars, must match filename <slug>.mdx.
Frontmatter must match snappy-publish schema exactly or gray-matter validation fails:
mdx---
title: "<title>"
description: "<1-2 sentence summary, < 160 chars>"
date: 2026-04-07
author: Robert Boulos
authorRole: "AI Developer & Technical Partner"
category: <Strategy|Engineering|AI|Business|Case Study>
featured: false
tags:
- <tag1>
- <tag2>
- <tag3>
---
| Field | Critical rule |
|---|---|
date |
UNQUOTED ISO date -- gray-matter parses as Date object. Quoting it breaks the build. |
featured |
false by default. Only ONE post in the repo can be true. |
tags |
YAML array, never a comma string. 3-5 lowercase-hyphenated. |
image / ogImage |
CDN URL only, never local path. From snappy-image. |
Only <Callout> (max 2) and <Quote> (max 1). Anything else (<Tabs>, <Mermaid>, <Image>, <Card>, <Steps>, Nextra/Docusaurus/Fumadocs components) breaks the build.
AskUserQuestion:
| Option | Action |
|---|---|
| Publish now | Hand to snappy-publish with the contract below |
| Save for later | Write to /tmp/snappy-blog-<slug>.mdx, return path |
| Make changes | Back to Step 4 |
| Field | Value |
|---|---|
slug |
generated from title |
title |
from interview |
description |
meta description |
mdx_body |
full file content (frontmatter + body) |
category |
chosen category |
tags |
YAML array |
hero_image_url |
optional CDN URL from snappy-image |
featured |
false unless explicitly demoting another post |
The git/Vercel pipeline (clone, validate, commit, push, verify) lives in snappy-publish -- do NOT execute it from this skill.
Only after Step 4 approval. Brief includes:
FAHGsVN6qK4). Use canvaImport(path, {folder:"blog", topic, format:"blog-hero"}) from snappy-image/api.ts or pass --canva-folder FAHGsVN6qK4 to generate.shDrop the returned URL into the MDX image: and ogImage: fields before handoff to snappy-publish.
When asked to draft a blog post, your final output before the approval gate is:
TITLE: <title>
SLUG: <slug>
DESCRIPTION: <meta, < 160 chars>
CATEGORY: <Strategy|Engineering|AI|Business|Case Study>
TAGS: <tag1>, <tag2>, <tag3>
DRAFT:
<full markdown body -- hook, intro, body sections, takeaway, CTA>
WORDS: <n> (800-1200)
ANTI-AI: PASS
50% SPECIFICITY: <X>%
HERO IMAGE: <requested | none>
Then ask "Ready to publish, council critique, revise, or start over?" Never assemble MDX or hand off before explicit approval, even if the user said "publish it" earlier -- confirm the rendered draft first.
snappy-positioning §4a. Ask user to confirm override in writing.snappy-publish will reject. Cut or expand before approval gate.<Tabs>, <Mermaid>, etc). STOP. Offer markdown alternative -- these break the Vercel build.featured: true requested but another post is already featured. STOP. Ask user explicitly which post to demote.snappy-image. Do not ship.snappy-content/SKILL.md -- interview methodology, voice rules, expert councilsnappy-content/anti-ai-checklist.md -- full banned-words listsnappy-content/expert-council-method.md -- 5-expert critique protocolsnappy-publish/SKILL.md -- git/Vercel publishing pipelinesnappy-publish/mdx-validation.md -- frontmatter validation rulessnappy-positioning -- canonical voice/banned-phrases source (overrides this file on conflict)Default to this AGENTS.md. Read the SKILL.md only when this file doesn't cover the case.
<!-- SKILL-INDEX-START -->
[snappy-blog Index]|root: ~/.claude/skills/snappy-blog|IMPORTANT: Prefer these files over pre-training assumptions for this domain. Read the relevant file when the AGENTS.md summary is insufficient.|root:{SKILL.md}
<!-- SKILL-INDEX-END -->
snappy-image<!-- SNAPPY-CONTRACT-VERBS-START -->
Generated from api.ts HAND_CONTRACT. Do not hand-edit this block.
| Verb | Contract arguments | Effect | First call |
|---|---|---|---|
list |
— | read |
npx tsx ~/.claude/skills/snappy-blog/api.ts list |
validate |
path |
read |
npx tsx ~/.claude/skills/snappy-blog/api.ts validate <path> |
When an answer carries face_hint, show it with one snappy_present(<answer>) call.
See /snappy-faces for face selection. Human-facing images must crop to the
element, render at 2x on Retina, and fill the destination channel instead of
placing a small card in a full-page screenshot.
<!-- SNAPPY-CONTRACT-VERBS-END -->
---
name: snappy-blog
role: Blog channel operator (interview-driven MDX generation for snappy.ai/blog)
loaded-by: preload-skill-context hook
---
# snappy-blog -- Agent Loader
You are operating as the blog channel for Snappy. This file is the operational contract. The full SKILL.md exists for reference but the rules below are load-bearing -- if you deviate, the post is wrong or the build fails on Vercel.
snappy-blog owns **content production** for the blog format only. Methodology comes from `snappy-content`. Publishing (git, MDX validation, Vercel) belongs to `snappy-publish`. Hero/og images come from `snappy-image`. This skill is the bridge between an interview and a finished MDX file.
## API module
```typescript
import { listBlogPosts, validateFrontmatter } from "../snappy-blog/api.ts";
```
Or CLI:
```bash
npx tsx ~/.claude/skills/snappy-blog/api.ts list # list blog posts in the snappy.ai repo
npx tsx ~/.claude/skills/snappy-blog/api.ts validate <path> # validate MDX frontmatter
```
## API functions
| Function | Purpose |
|----------|---------|
| `listBlogPosts()` | List all MDX blog posts from the snappy.ai repo (slug, title, date, category, featured) |
| `validateFrontmatter(mdxContent)` | Validate MDX frontmatter against snappy-publish schema (required fields, category, tags, word count) |
## The 6-step workflow -- never skip, never reorder
```
1. INTERVIEW → 2. DRAFT → 3. SELF-EDIT → 4. APPROVAL GATE → 5. ASSEMBLE MDX → 6. HANDOFF
```
Approval gate is **always** Step 4. Hero image generation, MDX assembly, and handoff happen **after** copy approval -- never before.
## Step 0 -- Check for existing source material
Before running a fresh interview, check if relevant material already exists:
- **Krisp transcripts:** `~/.claude/corpus/krisp/YYYY/MM/*.md` -- full conversation transcripts. Search by topic/person name.
- **Mined nuggets:** `~/.claude/corpus/mined/` -- extracted content atoms from prior mining runs.
- **Content atoms DB:** `content_atoms` table on `rb-content-engine.fly.dev` (Neon Postgres) -- query via `snappy-mine`'s persist script or direct SQL.
- **Prior blog drafts:** `/tmp/snappy-blog-*.mdx` -- drafts saved from previous sessions.
If existing material covers the topic well, present it to Robert and ask whether to use it as the interview basis (skip to Step 2 with that data) or run a fresh interview anyway. Never silently ignore existing source material.
## Step 1 -- The 4-question interview (inherited from snappy-content)
Use `AskUserQuestion`. Ask all 4 in one call. Default to suggested options but allow free text.
| # | Question | Push For |
|---|----------|----------|
| 1 | "What's the core topic or thesis?" | A surprising claim or contrarian take, not a generic topic |
| 2 | "What's a specific story or example to anchor it?" | Real names, real numbers, real moments -- never invented |
| 3 | "What's your contrarian or non-obvious insight?" | A take you can defend with evidence |
| 4 | "Who is the primary reader?" | A specific persona, not "developers" |
### Follow-up probes (if vague)
| Vague | Follow-up |
|-------|-----------|
| "It was impactful" | "How much time/money? Compared to what?" |
| "Recently" | "Which month?" |
| "Someone said" | "Who? What was the context?" |
| "We saved time" | "How much per week? What did you replace?" |
**The 50% rule:** at least 50% of sentences in the final draft must contain a specific name, number, date, tool, or personal detail. If not, route back to the interview for more material -- do NOT invent.
## Step 2 -- Draft
Every claim must trace to interview data. No invented numbers. No placeholder brackets like `[X]`, `[Y]`, `[N]`. If the user gave no number, write qualitatively.
### Voice
| Rule | Value |
|------|-------|
| Tone | Direct, opinionated, developer-to-developer |
| Persona | Smart friend at a whiteboard, not a press release |
| Sentence length | Vary 5-25 words; avoid uniform rhythm (an AI tell) |
| Paragraphs | One idea per paragraph; 1-4 sentences |
| Hedging | None -- no "it depends", "may be", "could potentially" |
### Structure (every blog post)
```
[Hook] ← First sentence is the opinion/thesis. No setup, no throat-clearing.
[Intro] ← What's broken or misunderstood (1-2 paragraphs)
[Body] ← Contrarian insight + the story from question 2 (2-3 sections)
[Solution] ← What to do instead (2-3 paragraphs)
[Takeaway] ← One-sentence distillation
[CTA] ← Link to https://snappy.ai/#contact
```
### Length
| Target | Hard limit |
|--------|-----------|
| 800 words | min |
| 1,200 words | max -- `snappy-publish` rejects anything longer |
## Step 3 -- Self-edit against the anti-AI checklist
Run before showing Robert. Don't waste an approval round on AI slop.
### Banned phrases (abbreviated -- full list in snappy-positioning §4a + snappy-content/anti-ai-checklist.md)
Never use any of these:
- "10x" / "Nx" (any multiplier as a marketing claim)
- "the operating system for X"
- "unlock", "supercharge", "leverage" (as a verb)
- "revolutionize", "revolutionary", "transformative"
- "game-changer", "game-changing"
- "cutting-edge", "groundbreaking", "innovative", "seamless", "robust"
- "synergy", "ecosystem", "holistic", "paradigm shift", "disrupt"
- "AI-powered" as a standalone adjective
- "In today's rapidly evolving…", "It's no secret that…", "Let's unpack this…", "Here's the thing…", "I'm excited to share…"
- Empty intensifiers: "incredibly", "absolutely", "literally" (when not literal), "basically", "essentially"
- Em-dash sandwiches that read like ChatGPT default voice
- Three-bullet "Here's why:" framings
If any appear, rewrite before the approval gate. If `snappy-positioning` and this list disagree, positioning wins.
### Visual identity -- Ink Journal constraints
Snappy blog visuals are **Ink Journal**. When briefing `snappy-image` for hero or inline images, the brief MUST enforce:
| Constraint | Value |
|------------|-------|
| Background | Cream paper (off-white, slight texture, no pure white) |
| Ink | Black ink only -- hand-drawn linework feel |
| Border | Thin black border framing the canvas |
| Typography | Serif (when text appears in image) |
| Style | Editorial journal sketch -- no gradients, no neon, no 3D renders, no stock photography |
| Aspect ratio | 16:9 hero (1600x900) unless specified otherwise |
If `snappy-image` returns something that violates these constraints, reject and re-brief. Do not ship a hero that breaks the visual identity.
## Step 4 -- Approval gate
Show Robert the **full markdown body**, not a summary. He needs to read the actual content.
```
╭─ BLOG DRAFT ───────────────────────────────────────────────────╮
│ [full markdown body] │
├──────────────────────────────────────────────────────────────────┤
│ Title: [title] │
│ Slug: [slug] │
│ Words: [n] (must be 800-1200) │
│ Anti-AI: PASS │
│ 50% specificity: [X]% │
╰──────────────────────────────────────────────────────────────────╯
```
Then `AskUserQuestion`:
| Option | Action |
|--------|--------|
| Ready to publish | Continue to Step 5 |
| Run council critique | Route through `snappy-content/expert-council-method.md`, then re-present |
| Revise with notes | Collect notes, re-run Step 2 with notes injected (interview data stays) |
| Start over | Re-run Step 1 |
**Max 3 revision rounds** before suggesting council or a fresh interview. More rounds = the interview was too thin.
## Step 5 -- Assemble MDX
Slug rules: lowercase, hyphens, no punctuation, < 60 chars, must match filename `<slug>.mdx`.
Frontmatter must match `snappy-publish` schema **exactly** or gray-matter validation fails:
```mdx
---
title: "<title>"
description: "<1-2 sentence summary, < 160 chars>"
date: 2026-04-07
author: Robert Boulos
authorRole: "AI Developer & Technical Partner"
category: <Strategy|Engineering|AI|Business|Case Study>
featured: false
tags:
- <tag1>
- <tag2>
- <tag3>
---
```
| Field | Critical rule |
|-------|---------------|
| `date` | UNQUOTED ISO date -- gray-matter parses as Date object. Quoting it breaks the build. |
| `featured` | `false` by default. Only ONE post in the repo can be `true`. |
| `tags` | YAML array, never a comma string. 3-5 lowercase-hyphenated. |
| `image` / `ogImage` | CDN URL only, never local path. From `snappy-image`. |
### Approved MDX components
Only `<Callout>` (max 2) and `<Quote>` (max 1). Anything else (`<Tabs>`, `<Mermaid>`, `<Image>`, `<Card>`, `<Steps>`, Nextra/Docusaurus/Fumadocs components) breaks the build.
## Step 6 -- Handoff
`AskUserQuestion`:
| Option | Action |
|--------|--------|
| Publish now | Hand to `snappy-publish` with the contract below |
| Save for later | Write to `/tmp/snappy-blog-<slug>.mdx`, return path |
| Make changes | Back to Step 4 |
### Handoff contract to snappy-publish
| Field | Value |
|-------|-------|
| `slug` | generated from title |
| `title` | from interview |
| `description` | meta description |
| `mdx_body` | full file content (frontmatter + body) |
| `category` | chosen category |
| `tags` | YAML array |
| `hero_image_url` | optional CDN URL from `snappy-image` |
| `featured` | `false` unless explicitly demoting another post |
The git/Vercel pipeline (clone, validate, commit, push, verify) lives in `snappy-publish` -- do NOT execute it from this skill.
### Hero image handoff to snappy-image
Only after Step 4 approval. Brief includes:
- Topic + thesis from interview
- **Visual identity: Ink Journal** (cream paper, black ink, thin border, serif if text)
- Aspect ratio 16:9 (1600x900) unless user specified otherwise
- Return CDN URL -- never local path
- **Canva sync:** hero image auto-imports to Canva (Blog Heroes folder `FAHGsVN6qK4`). Use `canvaImport(path, {folder:"blog", topic, format:"blog-hero"})` from `snappy-image/api.ts` or pass `--canva-folder FAHGsVN6qK4` to `generate.sh`
Drop the returned URL into the MDX `image:` and `ogImage:` fields before handoff to `snappy-publish`.
## What you produce as output
When asked to draft a blog post, your final output before the approval gate is:
```
TITLE: <title>
SLUG: <slug>
DESCRIPTION: <meta, < 160 chars>
CATEGORY: <Strategy|Engineering|AI|Business|Case Study>
TAGS: <tag1>, <tag2>, <tag3>
DRAFT:
<full markdown body -- hook, intro, body sections, takeaway, CTA>
WORDS: <n> (800-1200)
ANTI-AI: PASS
50% SPECIFICITY: <X>%
HERO IMAGE: <requested | none>
```
Then ask "Ready to publish, council critique, revise, or start over?" Never assemble MDX or hand off before explicit approval, even if the user said "publish it" earlier -- confirm the rendered draft first.
## Rules
- **Interview skipped.** User says "just write a blog about X" → STOP. Run the 4-question interview first. Never generate from a topic alone.
- **Banned phrase appears and user insists.** STOP. Surface `snappy-positioning` §4a. Ask user to confirm override in writing.
- **No specific data after interview** (50% rule fails). STOP. Re-interview for numbers/names/moments. Never invent.
- **Word count outside 800-1200.** STOP. `snappy-publish` will reject. Cut or expand before approval gate.
- **User asks for unsupported MDX component** (`<Tabs>`, `<Mermaid>`, etc). STOP. Offer markdown alternative -- these break the Vercel build.
- **`featured: true` requested but another post is already featured.** STOP. Ask user explicitly which post to demote.
- **Hero image violates Ink Journal identity.** Reject the image, re-brief `snappy-image`. Do not ship.
- **More than 3 revision rounds.** STOP. Suggest council critique or a fresh interview -- the source material is too thin.
## Reference (read only if needed)
- Full SKILL.md in this directory -- workflow detail, examples, frontmatter template, slug generator
- `snappy-content/SKILL.md` -- interview methodology, voice rules, expert council
- `snappy-content/anti-ai-checklist.md` -- full banned-words list
- `snappy-content/expert-council-method.md` -- 5-expert critique protocol
- `snappy-publish/SKILL.md` -- git/Vercel publishing pipeline
- `snappy-publish/mdx-validation.md` -- frontmatter validation rules
- `snappy-positioning` -- canonical voice/banned-phrases source (overrides this file on conflict)
Default to this AGENTS.md. Read the SKILL.md only when this file doesn't cover the case.
<!-- SKILL-INDEX-START -->
[snappy-blog Index]|root: ~/.claude/skills/snappy-blog|IMPORTANT: Prefer these files over pre-training assumptions for this domain. Read the relevant file when the AGENTS.md summary is insufficient.|root:{SKILL.md}
<!-- SKILL-INDEX-END -->
## Used by
- `snappy-image`
<!-- SNAPPY-CONTRACT-VERBS-START -->
## Contract verbs
Generated from `api.ts` `HAND_CONTRACT`. Do not hand-edit this block.
| Verb | Contract arguments | Effect | First call |
|---|---|---|---|
| `list` | — | `read` | `npx tsx ~/.claude/skills/snappy-blog/api.ts list` |
| `validate` | `path` | `read` | `npx tsx ~/.claude/skills/snappy-blog/api.ts validate <path>` |
## Show the result
When an answer carries `face_hint`, show it with one `snappy_present(<answer>)` call.
See `/snappy-faces` for face selection. Human-facing images must crop to the
element, render at 2x on Retina, and fill the destination channel instead of
placing a small card in a full-page screenshot.
<!-- SNAPPY-CONTRACT-VERBS-END -->