Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions routines/_skills/write-blog-draft/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Routine picks mode at runtime based on what's available — the routine doesn't
|---|---|---|
| **`thesis`** | Persona + audit + vision + positioning | Default. General positioning posts. NO Knowcap source needed. |
| **`case-study`** | A specific Knowcap recording + its confirmed memories + persona | When the routine finds at least 1 source in Demo org for the target persona with ≥3 confirmed memories. |
| **`comparison`** | Competitor research + persona + audit | When `docs/research/competitors/<name>/positioning.md` is fresher than 30 days AND not already covered in last 5 shipped blogs. |
| **`comparison`** | Competitor research + persona + audit | When `../claude-knowcap/company/docs/research/competitors-*.md` is fresher than 30 days AND not already covered in last 5 shipped blogs. |

Each mode has its own prompt template below.

Expand All @@ -25,9 +25,9 @@ target_keyword: "<string>"
target_keyword_5y_mena_interest: <0-100 OR null if Trends unavailable>

# Always required (brand DNA lives in the claude-knowcap hub — sibling repo, NOT this repo's docs/)
vision_md: "<text>" # from ../claude-knowcap/knowledge/strategies/VISION.md
positioning_md: "<text>" # from ../claude-knowcap/knowledge/strategies/POSITIONING.md
persona_section_md: "<text>" # the persona's section from ../claude-knowcap/knowledge/people/PRODUCT-PERSONAS.md
vision_md: "<text>" # from ../claude-knowcap/company/docs/strategy/vision.md
positioning_md: "<text>" # from ../claude-knowcap/company/docs/strategy/POSITIONING.md
persona_section_md: "<text>" # the persona's section from ../claude-knowcap/company/docs/research/product-personas.md
recent_shipped_slugs: [...] # last 20 slugs from app/content/blog/
recent_drafts_in_pipeline: [...] # to avoid double-drafting

Expand All @@ -43,14 +43,14 @@ knowcap_sources:
facts: [...]

# Required only when mode = comparison
competitor_positioning_md: "<text>" # from ../claude-knowcap/knowledge/topics/research/competitors/<name>/positioning.md
competitor_positioning_md: "<text>" # from ../claude-knowcap/company/docs/research/competitors-<name>-positioning.md

# Optional enhancement (any mode)
available_screenshots:
- slug: "verification-inbox"
alt: "<alt text>"
caption: "<caption>"
file: "../claude-knowcap/knowledge/product/screenshots/verification-inbox/full.png"
file: "../claude-knowcap/company/docs/product/screenshots/verification-inbox/full.png"
features: [verification, inbox]
personas: [odoo-partners, mena-audit-firms]
```
Expand Down Expand Up @@ -80,12 +80,19 @@ target_keyword_5y_mena_interest: {target_keyword_5y_mena_interest}
geo_score: <0-100 per GEO-AUDIT rubric, your honest estimate>
est_word_count: <draft word count>
draft_date: <today YYYY-MM-DD>
description: "<SEO meta description — 120-165 chars, plain English, no keyword stuffing>"
tags: [<5-8 kebab-case tags derived from persona + keyword + topic>]
author: "Hassan Arslan"
lang: "en"
dir: "ltr"
source_knowcap_ids: [<list only if mode=case-study, else []>]
embedded_screenshots: [<list of screenshot slugs used, else []>]
status: draft
---
```

`description`, `tags`, `author`, `lang`, `dir` are consumed by `scripts/publish-draft.mjs` on PR merge to populate the live blog post's frontmatter. Always include them — the publish script errors if `description` is missing.

## Mode `thesis` — the prompt

```
Expand Down
19 changes: 13 additions & 6 deletions routines/blogger/AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,20 @@ hit a quota.
2. **Digest** — surface the top picks to Hassan in the run window / Claude agents sidebar.
3. **Burn-state** — read `burn-state.json`; decide if today is a blog-gen day (see ROUTINE.md
"Daily flow + burn cadence"). If not → done for today (insights only).
4. **Blog day** — pick the persona's top fresh opportunity as `target_keyword`. Assemble inputs
(persona section, VISION, POSITIONING from the hub; shipped slugs; queue row's volume+comp).
5. **Mode** — Knowcap MCP Demo-org → persona project → source with ≥3 human-confirmed memories?
→ **case-study** (cite `source_knowcap_ids`, generate the verification panel via
`lib/gen-verification-panel.mjs`). Else → **SEO-grounded thesis** (answer the keyword's real
SERP / People-Also-Ask demand with a unique Knowcap angle).
4. **Blog day** — pick the persona's top fresh EN opportunity as `target_keyword`. Assemble inputs
(persona section from `../claude-knowcap/company/docs/research/product-personas.md`,
VISION from `../claude-knowcap/company/docs/strategy/vision.md`,
POSITIONING from `../claude-knowcap/company/docs/strategy/POSITIONING.md`;
shipped slugs; queue row's volume+comp).
5. **Mode** — try in order:
(1) **case-study**: Demo-org → persona project → source with ≥3 confirmed memories → cite `source_knowcap_ids`, generate verification panel via `lib/gen-verification-panel.mjs`.
(2) **comparison**: freshest `../claude-knowcap/company/docs/research/competitors-*.md` < 30d AND not covered in last 5 shipped → compare.
(3) **thesis**: default.
6. **Write** via write-blog-draft SKILL → run ALL gates (banned words, 1300–1600 words, keyword
in title+H2+≥3, slug unique, exactly 5 FAQ, frontmatter). Fail → regenerate/skip, never ship thin.
7. **Output** — draft → `docs/content-pipeline/drafts/<slug>.md`, open `[blog-draft]` PR to main.
Always report live URL: `https://knowcap.ai/blog/<slug>` — post goes live automatically on PR merge
via `.github/workflows/publish-blog-draft.yml` + `scripts/publish-draft.mjs` (no manual move needed).
Update `burn-state.json` (total_posts++, posts_this_week++).

## Hard rules
Expand All @@ -51,3 +56,5 @@ hit a quota.
- `state.json` — persona rotation cursor (gitignored)
- `lib/gen-verification-panel.mjs` — data-driven Knowcap UI SVG (case-study posts)
- `runs/<stamp>/` — per-run digest + draft + report (gitignored)
- `../../.github/workflows/publish-blog-draft.yml` — auto-publish on `[blog-draft]` PR merge
- `../../scripts/publish-draft.mjs` — frontmatter transform: draft fields → blog fields, moves draft to `app/content/blog/`
41 changes: 20 additions & 21 deletions routines/blogger/ROUTINE.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,27 +39,26 @@ After each shipped draft PR: `total_posts++`, `posts_this_week++`. Reset `posts_
- For each candidate source, pull confirmed memories (>= 3 required)
- If 1+ candidate qualifies → mode = case-study, pick the most recent
3. If no case-study candidate, try `comparison` mode:
- Check ../claude-knowcap/knowledge/topics/research/competitors/<name>/positioning.md mtime
- If freshest competitor doc is < 30 days old AND not covered in last 5 shipped → mode = comparison
- List ../claude-knowcap/company/docs/research/competitors-*.md, check mtime of each
- If freshest competitor doc is < 30 days old AND topic not covered in last 5 shipped → mode = comparison
4. Default to `thesis` mode
```

The first dry-run picked `thesis` mode (Demo org empty, no fresh competitor doc < 30d). That's the expected path until Demo org gets seeded.
The first few runs pick `thesis` mode (Demo org empty, no fresh competitor doc < 30d). Expected until Demo org gets seeded.

## Knowledge base (hub) — paths

Brand DNA, personas, research, and screenshots migrated to the `claude-knowcap` hub (PR #40, 2026-06-11). They are NO LONGER under this repo's `docs/`. Hub paths below are written relative to this repo root (`knowcap-website/`) — the hub is a sibling repo under `knowcap/`:
Brand DNA, personas, research, and screenshots live in the `claude-knowcap` hub (PR #40 2026-06-11, restructured into `company/docs/` 2026-06-15). NOT under this repo's `docs/`. Hub paths below are relative to this repo root (`knowcap-website/`) — the hub is a sibling repo under `knowcap/`:

| Input | Hub path (relative to repo root) |
|---|---|
| Personas | `../claude-knowcap/knowledge/people/PRODUCT-PERSONAS.md` |
| Vision | `../claude-knowcap/knowledge/strategies/VISION.md` |
| Positioning | `../claude-knowcap/knowledge/strategies/POSITIONING.md` |
| SEO audits | `../claude-knowcap/knowledge/topics/research/audits/SEO-AUDIT-*.md` |
| Competitors | `../claude-knowcap/knowledge/topics/research/competitors/<name>/positioning.md` |
| Screenshots index | `../claude-knowcap/knowledge/product/screenshots/_index.json` |
| Personas | `../claude-knowcap/company/docs/research/product-personas.md` |
| Vision | `../claude-knowcap/company/docs/strategy/vision.md` |
| Positioning | `../claude-knowcap/company/docs/strategy/POSITIONING.md` |
| Competitors | `../claude-knowcap/company/docs/research/competitors-*.md` (one file per competitor, e.g. `competitors-read-ai-positioning.md`) |
| Screenshots index | `../claude-knowcap/company/docs/product/screenshots/_index.json` |

Outputs stay in THIS repo: drafts → `docs/content-pipeline/drafts/`, shipped → `app/content/blog/`.
Outputs stay in THIS repo: drafts → `docs/content-pipeline/drafts/`, shipped → `app/content/blog/` (auto-moved on PR merge).

## Persona rotation state

Expand All @@ -74,18 +73,18 @@ Each run reads `cursor`, picks `personas[cursor]`, then advances `cursor = (curs
## Inputs (in execution order)

1. **Read persona rotation state** from `routines/blogger/state.json` (gitignored; if absent, default to index 0 = `odoo-partners` and create it)
2. **Read `../claude-knowcap/knowledge/people/PRODUCT-PERSONAS.md`** → pick the persona's section (study segment names map to slugs: "Odoo implementation partners" → `odoo-partners`, "Audit / accounting firms" → `mena-audit-firms`, etc.)
3. **Read `../claude-knowcap/knowledge/strategies/VISION.md`** → voice + anti-positioning
4. **Read `../claude-knowcap/knowledge/strategies/POSITIONING.md`** → three sentences + anti-positioning
5. **Run `node routines/blogger/scripts/seo-pull.mjs`** (live SEO engine, replaces the old static SEO-audit scan AND the dead Google Trends step). Pulls DataForSEO Google-Ads keyword demand for MENA (KSA + Egypt + UAE) in EN + AR, expands persona seeds into real related keywords with **search volume + competition**, filters to Knowcap ICP intent, ranks by `volume × competition-weight`, dedups against shipped posts, and writes `routines/blogger/opportunity-queue.json` + a digest. **`target_keyword` = the persona's top fresh (uncovered) opportunity** from the queue. Auth: DataForSEO creds in `~/.claude/secrets/blogger.md`.
6. **(Google Trends removed.)** DataForSEO volume + competition from step 5 is the demand signal — Trends was near-zero for this B2B ICP. Record the chosen keyword's `search_volume` + `competition` in frontmatter.
2. **Read `../claude-knowcap/company/docs/research/product-personas.md`** → pick the persona's section (study segment names map to slugs: "Odoo implementation partners" → `odoo-partners`, "Audit / accounting firms" → `mena-audit-firms`, etc.)
3. **Read `../claude-knowcap/company/docs/strategy/vision.md`** → voice + anti-positioning
4. **Read `../claude-knowcap/company/docs/strategy/POSITIONING.md`** → three sentences + anti-positioning
5. **Run `node routines/blogger/scripts/seo-pull.mjs`** (live SEO engine). Pulls DataForSEO Google-Ads keyword demand for MENA (KSA + Egypt + UAE) in EN + AR, expands persona seeds into real related keywords with **search volume + competition**, filters to Knowcap ICP intent, ranks by `volume × competition-weight`, dedups against shipped posts, and writes `routines/blogger/opportunity-queue.json` + a digest. **`target_keyword` = the persona's top fresh (uncovered) EN opportunity** from the queue. Auth: DataForSEO creds in `~/.claude/secrets/blogger.md`.
6. DataForSEO volume + competition from step 5 is the demand signal. Record the chosen keyword's `search_volume` + `competition` in frontmatter.
7. **Try `case-study` mode:**
- Query Knowcap MCP `mcp__knowcap__list_sources` → Demo org, persona project
- For each source, `mcp__knowcap__list_memories` filtered to source via `metadata.source_id`
- Pick source with ≥3 confirmed memories tagged with target_keyword OR persona
- If found: assemble `knowcap_sources` input array
8. **If no case-study candidate, try `comparison` mode:**
- List `../claude-knowcap/knowledge/topics/research/competitors/*/positioning.md` mtime
- List `../claude-knowcap/company/docs/research/competitors-*.md`, check mtime of each
- If freshest is < 30 days AND topic not in recent 5 shipped → mode = comparison
9. **Default to `thesis` mode** if neither condition met
10. **Pick matching screenshots** from `../claude-knowcap/knowledge/product/screenshots/_index.json`:
Expand All @@ -102,7 +101,7 @@ Each run reads `cursor`, picks `personas[cursor]`, then advances `cursor = (curs
| `mcp__knowcap__list_sources` | List Demo org sources for persona | `KNOWCAP_API_KEY` in `~/.claude.json` mcpServers.knowcap.env | read |
| `mcp__knowcap__list_memories` | Pull verified claims from picked sources | same | read |
| `mcp__knowcap__get_source` | Get source title + duration + metadata | same | read |
| Google Trends via `pytrends` | Keyword sizing (MENA-only, 5-year) | none (free) | read |
| DataForSEO Google Ads | Keyword volume + competition (replaces Trends) | creds in `~/.claude/secrets/blogger.md` | read |
| Filesystem | Read docs/, write `docs/content-pipeline/drafts/<slug>.md` | OS perms | read + write |
| `gh` CLI (or GitHub MCP when re-loaded) | Open PR | `gh auth` | write |

Expand All @@ -112,15 +111,15 @@ A single markdown file at `docs/content-pipeline/drafts/<slug>.md` with frontmat

PR body includes:
- Mode picked + why
- Persona + keyword + Google Trends signal
- Persona + keyword + volume/competition
- If case-study: source_knowcap_ids cited
- If screenshots embedded: which slugs from `_index.json`
- Validation gate pass/fail summary
- Link to `runs/<timestamp>/REPORT.md`
- Live URL: `https://knowcap.ai/blog/<slug>`

## Human confirms

- **Merge PR** = approve; next routine (`content-curator` or manual edit pass) handles polish before publish-to-blog (move from `docs/content-pipeline/drafts/` to `app/content/blog/`)
- **Merge PR** = approve → `.github/workflows/publish-blog-draft.yml` fires automatically, runs `scripts/publish-draft.mjs` to transform draft frontmatter and move the file to `app/content/blog/<slug>.md`, then commits to main. Vercel redeploys. Post is live within ~2 min. No manual move needed.
- **Close PR** = reject; routine logs the rejection reason to `runs/<timestamp>/rejected.txt` for future prompt-tuning

## Constraints
Expand Down
5 changes: 4 additions & 1 deletion scripts/publish-draft.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,10 @@ function buildPublishedFrontmatter(meta, body) {
pub.date = meta.draft_date || new Date().toISOString().slice(0, 10)
pub.author = meta.author || 'Hassan Arslan'

// Description — use meta.description if set, else auto-extract
// Description — required field; auto-extract only as last-resort fallback
if (!meta.description) {
console.warn(`Warning: draft missing description field — auto-extracting from body. Add description to frontmatter for better SEO.`)
}
pub.description = meta.description || extractDescription(body)

// Tags
Expand Down