--- 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/-/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//*/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 ' | 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\|` 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//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//-/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//-/ ✅ 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//-/`: - `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