- 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
14 KiB
name, description
| name | description |
|---|---|
| mm-content-writer | 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
# 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:
- Load
mm-blog-categoriesskill - Call it with the
related_clientfrom the brief - It will fetch categories (or use cache if fresh)
- 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:
# [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:
- Front-load the answer — first 150 words directly answer the core question
- 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)
- Structure for extraction:
- TL;DR or Key Takeaways box near top
- Comparison data → tables
- Sequential steps → numbered lists
- 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:
> **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)
- 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 - Check disk — run
find ~/vault/60_Articles -name "*.png"to inventory what already exists - Count total images needed: N_articles × (1 featured + inline_count) minus what exists on disk
- Estimate credits: ~1 credit/image for Nano Banana Pro
- Check FAL.ai balance: trigger
image_generateonce with a simple prompt. If it fails withExhausted balance, tell the user exactly how many credits are needed and stop - Surface the estimate: "Need ~{N} images = ~{N} FAL.ai credits. Current balance unknown — let me check."
- 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:
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)
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:
- The
image_generatetool returns a URL — download it immediately withcurl -sL "<url>" -o <path> - Verify the file exists and is non-empty (
file <path>) - 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:
# 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:
---
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 frontmatterbrief.md— Source brief (from Data Stage)images/featured.png— Featured imageimages/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
createddate = source research'sdate: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.mdto 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