Files
Kunthawat Greethong 7b5af16f6d Initial commit: moreminimore-service content pipeline
- 9 mm* skills (orchestrator, article, social, publish, analytics)
- 6 dependency skills (content-writer, geo-optimizer, etc.)
- 3 analytics scripts (GSC, Google Ads, Meta Ads)
- Config template + setup guide
- SOUL-MM.md persona extension
- OrbitOS integration reference
2026-07-02 10:01:53 +07:00

386 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: mm-content-writer
description: >
Content Stage: Takes a brief.md from Data Stage, writes an SEO-optimized article,
optimizes for GEO, generates images (Nano Banana), waits for approval,
then creates social posts (Facebook, X, IG) and saves everything to vault.
Use when: "เขียนบทความ", "write article from brief", "content-writer-mm",
"เริ่มเขียนบทความ", "สร้างบทความจาก brief".
---
# mm-content-writer
The main content production skill. Takes a brief from Data Stage and produces
a complete article + social posts + images, saved to vault.
## When This Must Trigger
- "เขียนบทความ", "write article from brief"
- "content-writer-mm", "mm-content-writer"
- "เริ่มเขียนบทความ", "สร้างบทความจาก brief"
- "สร้าง content จาก brief"
## Input
Read `brief.md` from `~/vault/60_Articles/<date>-<slug>/brief.md`.
The brief contains: topic, audience, tone, angles, outline, research data,
content requirements.
## Process
### Step 0: Determine client
Check if brief has `related_client` in frontmatter.
If not, ask which client:
```
บทความนี้สำหรับลูกค้าไหน?
1. [client-1] — [display_name]
2. [client-2] — [display_name]
```
Load client config from `~/vault/99_System/clients-config.json`:
- `contact` → used for CTA in **ad copy only** (not social posts)
- `website` → used for categories
### Step 1: Read the brief
```bash
# Find the most recent brief if not specified
ls -t ~/vault/60_Articles/*/brief.md 2>/dev/null | head -5
# Or search by client:
ls -t ~/vault/60_Articles/<client-id>/*/brief.md 2>/dev/null | head -5
```
Read the brief.md. Extract:
- Topic, audience, tone, word count target
- SEO keyword
- Outline (H1, H2s, key points per section)
- Research data (stats, quotes, examples)
- Content requirements (images, FAQ, schema)
If multiple briefs exist, ask which one to work on.
### Step 1.5: Load blog categories
Before writing, load the blog categories from the client's website to ensure
the article uses the correct category:
1. Load `mm-blog-categories` skill
2. Call it with the `related_client` from the brief
3. It will fetch categories (or use cache if fresh)
4. Present categories to user or auto-select based on article topic
If user wants a new category:
- WordPress: create via API using mm-blog-categories
- Astro: add to convention, update cache
Use the exact category name/slug from the cache.
### Step 2: Write the article
Write a complete SEO-optimized article following these rules:
**Title (H1)**:
- Hook-driven (number, contrarian claim, specific audience, curiosity gap)
- Contains primary keyword (front-loaded)
- Under 60 characters
**Opening**:
- Hook paragraph — not throat-clearing ("In today's digital landscape...")
- Directly addresses the search intent
- First 100 words contain the primary keyword
**Body**:
- Follow the outline from the brief
- Minimum 1000 words (use brief's target if higher)
- Short paragraphs (2-4 sentences)
- Bullet lists for scannability
- Bold key phrases
- One idea per paragraph
- Include specific examples, data, and quotes from the brief
- Internal links to related vault notes (as wikilinks for now)
**FAQ Section**:
- 3-5 questions targeting People Also Ask
- Direct, concise answers
**Structure**:
```markdown
# [Hook-driven title]
[Hook paragraph — 2-3 sentences that grab attention and contain primary keyword]
## Table of Contents
- [Section 1](#section-1)
- [Section 2](#section-2)
...
## [H2 — Section 1]
[Content with data from brief]
## [H2 — Section 2]
[Content with data from brief]
...
## FAQ
### [Question 1]
[Answer]
### [Question 2]
[Answer]
---
*Last updated: YYYY-MM-DD*
```
### Step 3: GEO Optimize
Apply GEO optimization to the article:
1. **Front-load the answer** — first 150 words directly answer the core question
2. **Evidence density**:
- ≥5 specific numbers with units
- ≥1 external citation per 500 words
- ≥2 direct quotes from named experts (from brief's research data)
- ≥3 named entities (people, orgs, products)
3. **Structure for extraction**:
- TL;DR or Key Takeaways box near top
- Comparison data → tables
- Sequential steps → numbered lists
4. **Strip anti-patterns**:
- No keyword stuffing
- No filler ("In today's digital landscape...")
- No unsupported superlatives
- No vague entities ("experts say")
Add a TL;DR section after the hook paragraph:
```markdown
> **TL;DR:** [2-3 sentence summary of the article's main point and takeaway]
```
### Step 4: Generate images
**CRITICAL RULE: NEVER delegate image generation to subagents.** `image_generate` is sequential and slow (15-30s per call). Subagents timeout before completing. Always generate images from the main agent, one image per tool call.
If a subagent DOES generate images and then times out, the URLs **can still be recovered** via the FAL API history endpoint (see `references/image-generation-pitfalls.md` section 1b), but this recovery process is slower than generating fresh. Prevent the problem by never delegating image generation.
**CRITICAL RULE: Check FAL.ai billing balance BEFORE starting image generation.** If balance is exhausted, tell the user and stop — don't generate partial sets.
**Image cost budget**: Each article needs 1 featured + 3 inline minimum = 4 images minimum. For N articles, expect 4N image_generate calls. Batch of 14 articles = 56+ calls.
### Pre-generation checklist (MANDATORY before ANY image work)
1. **Check FAL API history FIRST** — run `curl -s --request GET --url 'https://api.fal.ai/v1/models/requests/by-endpoint?limit=100&sort_by=ended_at&expand=payloads' --header 'Authorization: Key <FAL_KEY>' | jq -r '.requests[] | select(.status == "COMPLETED") | .request.payload.json_output.images[].url'` to see if previously-generated images can be reused before spending new credits
2. **Check disk** — run `find ~/vault/60_Articles -name "*.png"` to inventory what already exists
3. **Count total images needed**: N_articles × (1 featured + inline_count) minus what exists on disk
4. **Estimate credits**: ~1 credit/image for Nano Banana Pro
5. **Check FAL.ai balance**: trigger `image_generate` once with a simple prompt. If it fails with `Exhausted balance`, tell the user exactly how many credits are needed and stop
6. **Surface the estimate**: "Need ~{N} images = ~{N} FAL.ai credits. Current balance unknown — let me check."
7. **Warn for large batches**: "14 articles × 4 images = 56 credits. This will consume significant balance. Proceed?"
### Post-generation reconciliation (MANDATORY after batch)
After ALL image generation is done, reconcile expected vs. actual:
```bash
cd ~/vault/60_Articles
echo "=== Total images on disk ==="
find . -name "*.png" | wc -l
echo "=== Per-article inventory ==="
for dir in */; do
featured=$(ls "$dir/images/" 2>/dev/null | wc -l)
attach=$(ls "$dir/attachments/" 2>/dev/null | wc -l)
echo "$dir → featured: $featured | inline: $attach"
done
echo "=== Remaining placeholders ==="
grep -rn "PLACEHOLDER\|TODO_IMAGE\|<!-- IMAGE" . --include="*.md" | grep -v "brief.md" || echo "None found"
```
If images are missing (expected > actual), report the exact shortfall and which articles are affected. **Do not silently declare completion if not all images exist.**
#### 4a. Generate featured image (REQUIRED for all modes)
```python
featured_prompt = f"""
Professional blog featured image for article about {topic}.
Style: clean, modern, {tone} aesthetic.
Subject: [specific visual concept from article]
Composition: centered, balanced, with negative space for text overlay.
Colors: [palette based on brand or topic]
No text in image. No stock photo clichés.
"""
```
Save to: `~/vault/60_Articles/<date>-<slug>/images/featured.png`
#### 4b. Generate inline images (batch mode = SKIP, single mode = REQUIRED)
**Batch mode**: Skip inline images entirely — generate ONLY the featured image. Inline images are a separate post-processing step the user must explicitly request. If they do request inline images for a batch, warn them about the cost (3× article count) and FAL.ai credit burn.
**Single mode**: Generate inline images per the brief's content requirements:
- Diagram or infographic for complex concepts
- Screenshot or example illustration
- Data visualization or comparison visual
Save each to: `~/vault/60_Articles/<date>-<slug>/attachments/<descriptive-name>.png`
#### 4c. Download and verify images (CRITICAL — do not skip)
For every generated image:
1. The `image_generate` tool returns a URL — download it immediately with `curl -sL "<url>" -o <path>`
2. Verify the file exists and is non-empty (`file <path>`)
3. If the download fails, retry once. If it still fails, report the failure
#### 4d. Replace all placeholders in article.md
Before writing `article.md`, ensure every inline image reference is a real path (not a `<!-- PLACEHOLDER -->` comment). Images referenced in the article MUST exist on disk already.
**Verification step after all images are saved:**
```bash
# Check every placeholder was replaced
grep -r "PLACEHOLDER" 60_Articles/<slug>/article.md
# Should return no matches
# Check every image file referenced in article.md exists on disk
# Extract all image paths and stat them
grep -oP '!\\[.*?\\]\\((.*?)\\)' article.md | while read -r line; do
path=$(echo "$line" | grep -oP '\\(.*?\\)' | tr -d '()')
full_path=$(dirname article.md)/$path
if [ ! -f "$full_path" ]; then
echo "MISSING: $full_path"
else
echo "OK: $full_path ($(wc -c < "$full_path") bytes)"
fi
done
```
#### 4e. Deduplicate image prompts
Track every image prompt you've already sent in the current session. If a new article needs a similar visual concept, reuse the existing image rather than generating another one. Common patterns that can share a single image:
- "architecture diagram" type visuals
- Flow/infographic layout images
- Generic "concept illustration" images
#### 4f. FAL.ai credit management
Track FAL.ai balance proactively:
- Don't assume balance is unlimited
- If balance runs out mid-session, stop and tell the user exactly how many images were generated vs. still needed
- When resuming after top-up, don't regenerate existing images — skip to the missing ones only
### Step 5: Assemble and present for approval
Create the complete article file:
```
~/vault/60_Articles/<client-id>/<date>-<slug>/article.md
```
Frontmatter:
```yaml
---
type: article
status: draft
created: YYYY-MM-DD # IMPORTANT: use source date from brief, NOT today
seo_keyword: "[primary keyword]"
title: "[article title]"
meta_description: "[120-160 chars]"
slug: "[url-slug]"
featured_image: "images/featured.png"
related_client: "[client-id or empty]"
published_to: []
related_website: ""
source_brief: "brief.md"
word_count: [N]
---
```
Present to user:
```
📝 บทความพร้อมตรวจ:
📄 [Article Title]
📊 [word count] words
🖼️ [N] images generated
🔍 SEO keyword: [keyword]
📋 GEO optimized: ✅
โครงสร้าง:
- H1: [title]
- H2: [section 1]
- H2: [section 2]
- ...
- FAQ: [N] questions
ตรวจบทความแล้วเป็นยังไงบ้าง?
- ✅ "approve" — อนุมัติ → สร้าง social posts
- ✏️ "แก้ไข [specify]" — ปรับแก้ตามที่ต้องการ
- ❌ "reject" — เริ่มใหม่
```
Wait for user response. If edits requested, apply and re-present.
### Step 5: Present for approval
Once the article is approved, the flow continues to `mm-publish-content` to publish
the article and get the published URL, then `mm-social-writer` creates social posts.
### Step 7: Final summary
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Article Ready: [Article Title]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📂 ~/vault/60_Articles/<client-id>/<date>-<slug>/
✅ article.md ← [word count] words, GEO optimized
✅ images/featured.png ← Featured image
✅ images/inline-01.png ← [description]
✅ images/inline-02.png ← [description]
✅ images/inline-03.png ← [description]
✅ brief.md ← Source brief
📋 NEXT STEPS:
1. Load mm-publish-content → publish article → get URL
2. Load mm-social-writer → create social posts (with URL)
3. Load mm-publish-content → publish social posts
```
## Output
All files in `~/vault/60_Articles/<client-id>/<date>-<slug>/`:
- `article.md` — Complete article with frontmatter
- `brief.md` — Source brief (from Data Stage)
- `images/featured.png` — Featured image
- `images/inline-*.png` — Inline article images
## Image Generation — Pitfalls Reference
See `references/image-generation-pitfalls.md` for detailed error transcripts, retry strategies, and FAL.ai balance troubleshooting from real sessions.
## Important Notes
- Article approval is MANDATORY before creating social posts (single-article mode)
- In **batch mode**, intermediate approval is skipped — the user implicitly approved by saying "ทั้งหมด"
- **Date convention**: article `created` date = source research's `date:` frontmatter, NOT today. Extract from the brief's source_research field
- Images are generated from the MAIN AGENT only — NEVER delegate image generation to subagents (see `references/image-generation-pitfalls.md`)
- After all images are generated AND saved, run verification: `grep -rn "PLACEHOLDER" article.md` to catch any missed replacements
- Article approval is MANDATORY before proceeding to publish
- Images are generated during the writing process, not after
- All frontmatter must include `type`, `status`, `created`
- Use Thai for article content unless the brief specifies otherwise
- GEO optimization is applied automatically — don't skip it
- After approval, use mm-publish-content to publish, then mm-social-writer for posts
- **Batch mode**: featured images only (no inline images). Generate exactly ONE image per article. Inline images are a separate pass with its own FAL.ai budget warning
- **Image verification**: After every image generation round (featured or inline), verify ALL image references in article.md resolve to real files on disk. A broken image link in the vault means a broken image when published