Files
opencode-skill/skills/seo-multi-channel/SKILL.md
Kunthawat Greethong 628298183a feat: migrate website-creator from Next.js+Payload to Astro+Tina CMS
Major changes:
- Replace Payload CMS with Tina CMS (self-hosted)
- Add Astro DB for consent logging (PDPA compliant)
- Update Tailwind v3 to v4 (@tailwindcss/vite plugin)
- Add astro-tina-starter template
- Rewrite consent template for Astro (ConsentBanner.astro, Astro DB, Nano Stores)
- Add install-tina-backend.sh for self-hosted Tina per customer
- Rename convert-astro.sh to migrate-tina.sh
- Add AGENTS.md template for generated websites
- Delete all Payload/Next.js files

Technical updates:
- Astro DB using defineDb with eq operators for queries
- Tailwind v4 with @theme block
- Tina CMS local development mode
- Proper Astro API routes for consent

Research-verified with official documentation (April 2026)
2026-04-17 14:52:59 +07:00

19 KiB

name, description
name description
seo-multi-channel Generate multi-channel marketing content (Facebook, Ads, Blog, X) with Thai language support and website-creator integration. Use when user wants to create content for multiple channels from a single topic.

🎯 SEO Multi-Channel Content Generator

Skill Name: seo-multi-channel
Category: deep
Load Skills: ['website-creator']


🚀 Purpose

Generate marketing content for multiple channels from a single topic with:

  • Priority Channels: Facebook > Facebook Ads > Google Ads > Blog > X (Twitter)
  • Thai Language Support: Full Thai text processing with PyThaiNLP
  • Image Handling: Browse website repo for product images, or ask user to provide
  • Website-Creator Integration: Auto-publish blog posts to Astro content collections
  • API-Ready Output: Structured JSON for future ad platform API integration
  • Per-Project Context: Context files in each website repo

Use Cases:

  1. Multi-Channel Campaign: One topic → Facebook post + Facebook Ads + Google Ads + Blog + X thread
  2. Social-Only: Facebook post + Facebook Ads for product promotion
  3. Blog-First: SEO blog post with auto-publish to website
  4. Ads-Only: Google Ads + Facebook Ads copy for existing product

📋 Pre-Flight Questions

MUST ask before generating:

  1. Topic/Subject: What topic do you want content about?

  2. Channels Needed: (Default: All channels)

    • Facebook (organic posts)
    • Facebook Ads (paid campaigns)
    • Google Ads (search campaigns)
    • Blog (SEO articles)
    • X/Twitter (threads)
  3. Content Type: (Auto-detect or ask)

    • Product/Service (requires product images)
    • Knowledge/Educational (generates fresh images)
    • Statistics/Data (generates infographic-style images)
    • Announcement/News (may not need images)
  4. Target Language: (Auto-detect from topic or ask)

    • Thai (default for Thai topics)
    • English
    • Bilingual (both Thai + English)
  5. For Product Content:

    • Product name
    • Website repo path (to browse for existing images)
    • Product URL (if available)
  6. For Blog Posts:

    • Target keyword for SEO
    • Should I auto-publish to website? (yes/no)
    • Website repo path (if auto-publish)
  7. Tone/Formality: (Auto-detect from context or default)

    • กันเอง (Casual) - for social media
    • ปกติ (Normal) - for blog
    • เป็นทางการ (Formal) - for corporate

🔄 Workflow

Phase 1: Context Loading

  1. Load Project Context:

    • Read context/brand-voice.md from website repo
    • Read context/target-keywords.md
    • Read context/seo-guidelines.md
    • Auto-detect formality level from brand voice
  2. Check Data Services:

    • Check if GA4 configured (skip if not)
    • Check if GSC configured (skip if not)
    • Check if DataForSEO configured (skip if not)
    • Check if Umami configured (skip if not)
    • Fetch available performance data
  3. Load Channel Templates:

    • Load YAML templates for selected channels
    • Apply brand voice customizations

Phase 2: Content Generation

For Each Channel:

Facebook (Organic):

Output:
  - primary_text: 125-250 chars (Thai can be longer)
  - headline: 100 chars max
  - hashtags: 3-5 recommended
  - cta: เลือกจาก ["เรียนรู้เพิ่มเติม", "สมัครเลย", "ซื้อเลย", "ดูรายละเอียด"]
  - image: Generated or edited
  - variations: 5 options

Facebook Ads:

Output:
  - primary_text: 125 chars recommended (5000 max)
  - headline: 40 chars
  - description: 90 chars
  - cta: Button choice
  - image: Product-focused or benefit-focused
  - variations: 5 options
  - api_ready: true (matches Meta Ads API structure)

Google Ads:

Output:
  - headlines: 15 variations (30 chars each)
  - descriptions: 4 variations (90 chars each)
  - keywords: Suggested keyword list
  - negative_keywords: Suggested negatives
  - ad_extensions: Sitelink, callout, structured snippets
  - api_ready: true (matches Google Ads API structure)

Blog (SEO Article):

Output:
  - markdown: Full article with frontmatter
  - word_count: 1500-3000 (Thai), 2000-3000 (English)
  - keyword_density: 1.0-1.5% (Thai), 1.5-2% (English)
  - meta_title: 50-60 chars
  - meta_description: 150-160 chars
  - slug: Auto-generated (Thai-friendly)
  - images: Saved to website repo
  - payload_cms_ready: true (REST API format)

X/Twitter Thread:

Output:
  - tweets: 5-10 tweet thread
  - hook_tweet: First tweet (280 chars)
  - body_tweets: 2-8 tweets (280 chars each)
  - cta_tweet: Final tweet with CTA
  - hashtags: 2-3 per tweet
  - thread_title: Optional title card

Phase 3: Image Handling

Product Content:

1. Browse website repo for existing product images:
   - Search: public/images/, src/assets/, **/*{product_name}*.{jpg,png,webp}
   
2. If images found:
   - Select best image (highest quality, product-focused)
   - Ask user: "Use this image or provide a new one?"
   
3. If no images found:
   - Ask user: "No product images found in repo. Please provide image path or URL."
   - Wait for user to provide

Non-Product Content:

1. Determine content type:
   - Service  Professional illustration
   - Knowledge  Educational visual metaphor
   - Stats  Infographic with charts
   - News  Clean, modern announcement style

2. Ask user to provide image:
   - "For this content, please provide an image or use: [suggestion]"

3. Save images:
   - Social/Ads  seo-multi-channel/generated-images/{topic}/{channel}/
   - Blog  {website-repo}/public/images/blog/{slug}/

Phase 4: Output & Publishing

Output Structure:

output/{topic-slug}/
├── facebook/
│   ├── posts.json
│   └── images/
├── facebook_ads/
│   ├── ads.json
│   └── images/
├── google_ads/
│   └── ads.json
├── blog/
│   ├── article.md
│   └── images/
├── x/
│   └── thread.json
└── summary.json

Auto-Publish Blog (if enabled):

1. Parse frontmatter from blog markdown
2. Detect language (Thai  'th', English  'en')
3. Generate slug (Thai-friendly: use transliteration or keep Thai)
4. Save to: {website-repo}/src/content/blog/({lang})/{slug}.md
5. Copy images to: {website-repo}/public/images/blog/{slug}/
6. Git commit: git add . && git commit -m "Add blog post: {slug}"
7. Git push: git push origin main (triggers Easypanel auto-deploy)
8. Return deployment URL

📁 Output Examples

Facebook Post Output:

{
  "channel": "facebook",
  "topic": "บริการ podcast",
  "language": "th",
  "generated_at": "2026-03-08T14:30:00+07:00",
  "variations": [
    {
      "id": "fb_post_1",
      "primary_text": "คุณกำลังมองหาวิธีเริ่มต้น podcast ใช่ไหม? 🎙️\n\nตอนนี้ใครๆ ก็ทำ podcast ได้ง่ายๆ แค่มีเครื่องมือที่เหมาะสม เราช่วยคุณได้ตั้งแต่เริ่มจนถึงเผยแพร่\n\n#podcast #podcastไทย #สร้างpodcast",
      "headline": "เริ่มต้น podcast ของคุณวันนี้",
      "cta": "เรียนรู้เพิ่มเติม",
      "hashtags": ["#podcast", "#podcastไทย", "#สร้างpodcast"],
      "image": {
        "type": "generated",
        "path": "output/บริการ-podcast/facebook/images/variation_1.png",
        "prompt": "Professional podcast studio setup with microphone and headphones, modern aesthetic, Thai-friendly design"
      },
      "api_ready": {
        "message": "Matches Meta Graph API /act_id/adcreatives structure",
        "endpoint": "POST /v18.0/act_{ad_account_id}/adcreatives"
      }
    }
  ]
}

Google Ads Output:

{
  "channel": "google_ads",
  "topic": "podcast hosting",
  "language": "th",
  "generated_at": "2026-03-08T14:30:00+07:00",
  "responsive_search_ads": [
    {
      "id": "ga_rsa_1",
      "headlines": [
        {"text": "บริการ Podcast Hosting", "pin": false},
        {"text": "เริ่มต้นฟรี 14 วัน", "pin": false},
        {"text": "เผยแพร่ทุกแพลตฟอร์ม", "pin": false},
        {"text": "ง่าย รวดเร็ว มืออาชีพ", "pin": false},
        {"text": "รองรับภาษาไทย", "pin": false}
      ],
      "descriptions": [
        {"text": "แพลตฟอร์ม podcast ที่ครบวงจรที่สุด เริ่มต้นสร้าง podcast ของคุณวันนี้"},
        {"text": "เผยแพร่ Apple Podcasts, Spotify, YouTube Music ได้ในคลิกเดียว"}
      ],
      "keywords": ["podcast hosting", "host podcast", "บริการ podcast", "แพลตฟอร์ม podcast"],
      "negative_keywords": ["ฟรี", "download", "mp3"],
      "ad_extensions": {
        "sitelinks": [
          {"text": "เริ่มฟรี 14 วัน", "url": "/free-trial"},
          {"text": "ดูคุณสมบัติ", "url": "/features"}
        ],
        "callouts": ["รองรับภาษาไทย", "ทีมซัพพอร์ท 24/7", "ยกเลิกเมื่อไหร่ก็ได้"]
      },
      "api_ready": {
        "matches": "Google Ads API v15.0",
        "endpoint": "POST /google.ads.googleads.v15.services/GoogleAdsService:Mutate",
        "resource": "AdGroupAd"
      }
    }
  ]
}

Blog Post Output:

---
title: "บริการ Podcast Hosting ที่ดีที่สุดปี 2026: คู่มือครบวงจร"
description: "เปรียบเทียบ 10+ บริการ podcast hosting พร้อมข้อมูลจริง ช่วยคุณเลือกแพลตฟอร์มที่เหมาะกับ podcast ของคุณ"
keywords: ["podcast hosting", "บริการ podcast", "แพลตฟอร์ม podcast", "host podcast"]
slug: podcast-hosting-best-2026
lang: th
category: guides
tags: [podcast, hosting, review]
created: 2026-03-08
images:
  - src: /images/blog/podcast-hosting-best-2026/hero.png
    alt: "เปรียบเทียบบริการ podcast hosting"
---

# บริการ Podcast Hosting ที่ดีที่สุดในปี 2026

คุณกำลังมองหาบริการ podcast hosting ที่ใช่อยู่ใช่ไหม? 🎙️

บทความนี้จะเปรียบเทียบแพลตฟอร์มยอดนิยม 10+ เจ้า พร้อมข้อมูลจริงจากการทดสอบ...

[Content continues for 2000+ words]

## สรุป

เลือกบริการ podcast hosting ที่เหมาะกับคุณที่สุด...

**พร้อมเริ่ม podcast ของคุณหรือยัง?** [สมัครฟรี 14 วัน →](/signup)

🔧 Technical Implementation

Thai Language Processing:

from pythainlp import word_tokenize, sent_tokenize
from pythainlp.util import normalize

def count_thai_words(text: str) -> int:
    """Count Thai words (no spaces between words)"""
    tokens = word_tokenize(text, engine="newmm")
    return len([t for t in tokens if t.strip() and not t.isspace()])

def calculate_thai_keyword_density(text: str, keyword: str) -> float:
    """Calculate keyword density for Thai text"""
    text_normalized = normalize(text)
    keyword_normalized = normalize(keyword)
    count = text_normalized.count(keyword_normalized)
    word_count = count_thai_words(text)
    return (count / word_count * 100) if word_count > 0 else 0

def detect_content_language(text: str) -> str:
    """Detect if content is Thai or English"""
    thai_chars = sum(1 for c in text if '\u0E00' <= c <= '\u0E7F')
    total_chars = len(text)
    thai_ratio = thai_chars / total_chars if total_chars > 0 else 0
    
    if thai_ratio > 0.3:
        return 'th'
    return 'en'

Image Handling:

import os
import glob
from pathlib import Path

def find_product_images(product_name: str, website_repo: str) -> List[str]:
    """Find existing product images in website repo"""
    extensions = ['.jpg', '.jpeg', '.png', '.webp']
    found_images = []
    
    search_patterns = [
        f"**/*{product_name}*{{ext}}" for ext in extensions
    ] + [
        f"public/images/**/*{{ext}}",
        f"src/assets/**/*{{ext}}"
    ]
    
    for pattern in search_patterns:
        matches = glob.glob(os.path.join(website_repo, pattern), recursive=True)
        found_images.extend(matches)
    
    return found_images[:10]  # Return top 10 matches

def save_image_for_channel(image_data: bytes, topic: str, channel: str) -> str:
    """Save generated/edited image to correct location"""
    if channel == 'blog':
        # Blog images go to website repo
        output_dir = os.path.join(website_repo, 'public/images/blog', topic_slug)
    else:
        # Social/Ads images go to separate folder
        output_dir = os.path.join('output', topic_slug, channel, 'images')
    
    os.makedirs(output_dir, exist_ok=True)
    image_path = os.path.join(output_dir, f"variation_{variation_num}.png")
    
    with open(image_path, 'wb') as f:
        f.write(image_data)
    
    return image_path

Website-Creator Integration:

def publish_blog_to_payload(
    article_md: str,
    website_url: str,
    payload_token: str
) -> Dict:
    """
    Publish blog post to Payload CMS via REST API
    Returns publication status
    """
    # Parse frontmatter
    frontmatter = parse_frontmatter(article_md)

    # Detect language
    lang = detect_content_language(article_md)

    # Generate slug
    slug = generate_slug(frontmatter['title'], lang)
    if lang == 'th':
        slug = f'th/{slug}'

    # Convert markdown to Lexical JSON
    content = markdown_to_lexical(article_md)

    # Prepare Payload document
    payload_doc = {
        'title': frontmatter['title'],
        'slug': slug,
        'content': content,
        'status': 'draft',
        'description': frontmatter.get('description', ''),
    }

    # Send to Payload CMS API
    response = requests.post(
        f'{website_url}/api/posts',
        headers={'Authorization': f'Bearer {payload_token}'},
        json=payload_doc
    )

    result = response.json()

    # Return publication info
    return {
        'published': True,
        'id': result.get('id'),
        'slug': slug,
        'language': lang,
        'admin_url': f'{website_url}/admin/collections/posts/{result.get("id")}',
        'api_url': f'{website_url}/api/posts',
    }

📐 Channel Specifications

Facebook:

  • Primary text: 125-250 chars (Thai can be longer)
  • Headline: 100 chars max
  • Hashtags: 3-5 recommended
  • Image: 1200x630 (1.91:1)
  • Variations: 5

Facebook Ads:

  • Primary text: 125 chars recommended (5000 max)
  • Headline: 40 chars
  • Description: 90 chars
  • CTA: Button selection
  • Image: 1200x628 (1.91:1) or 1080x1080 (1:1)
  • API ready: Yes (Meta Graph API)

Google Ads:

  • Headlines: 15 variations, 30 chars each
  • Descriptions: 4 variations, 90 chars each
  • Keywords: 15-20 suggested
  • Negative keywords: 10-15 suggested
  • Ad extensions: Sitelinks, callouts, structured snippets
  • API ready: Yes (Google Ads API)

Blog:

  • Word count: 1500-3000 (Thai), 2000-3000 (English)
  • Keyword density: 1.0-1.5% (Thai), 1.5-2% (English)
  • Meta title: 50-60 chars
  • Meta description: 150-160 chars
  • Images: Saved to website repo
  • Format: Markdown with frontmatter
  • Astro ready: Yes (content collections)

X/Twitter:

  • Hook tweet: 280 chars
  • Body tweets: 2-8 tweets, 280 chars each
  • CTA tweet: 280 chars
  • Hashtags: 2-3 per tweet
  • Thread title: Optional

⚙️ Environment Variables

Required (in unified .env or project .env):

# MiniMax API (for image generation via minimax-multimodal-toolkit)
MINIMAX_API_KEY=your_token_here

# Google Analytics 4 (optional)
GA4_PROPERTY_ID=G-XXXXXXXXXX
GA4_CREDENTIALS_PATH=path/to/ga4-credentials.json

# Google Search Console (optional)
GSC_SITE_URL=https://yourdomain.com
GSC_CREDENTIALS_PATH=path/to/gsc-credentials.json

# DataForSEO (optional)
DATAFORSEO_LOGIN=your_login
DATAFORSEO_PASSWORD=your_password

# Umami Analytics (optional, if self-hosted)
UMAMI_API_URL=https://analytics.yourdomain.com
UMAMI_API_KEY=your_api_key

🚀 Commands

Generate Multi-Channel Content:

python3 skills/seo-multi-channel/scripts/generate_content.py \
  --topic "บริการ podcast hosting" \
  --channels facebook facebook_ads google_ads blog x \
  --website-repo ./my-website \
  --auto-publish true

Generate for Specific Channel:

# Facebook Ads only
python3 skills/seo-multi-channel/scripts/generate_content.py \
  --topic "podcast microphone" \
  --channels facebook_ads \
  --product-name "PodMic Pro" \
  --website-repo ./my-website

Publish Existing Blog:

python3 skills/seo-multi-channel/scripts/publish_blog.py \
  --article drafts/podcast-guide-2026.md \
  --website-repo ./my-website

📊 Quality Scoring

Each piece of content is scored before output:

  1. Keyword Optimization (0-25 points)

    • Density, placement, variations
  2. Brand Voice Alignment (0-25 points)

    • Tone, terminology, style
  3. Channel Fit (0-25 points)

    • Length, format, CTA appropriateness
  4. Thai Language Quality (0-25 points)

    • Natural phrasing, formality level, no awkward translations

Minimum score: 70/100 to publish. Below 70 → auto-revise or flag for review.


⚠️ Important Notes

  1. Thai Word Counting: Thai has no spaces between words. Uses PyThaiNLP for accurate counting.

  2. Formality Detection: Auto-detects from brand voice context. Defaults to casual for social, normal for blog.

  3. Image Handling:

    • Product content → Browse repo first → Ask user to confirm or provide
    • Non-product → Ask user to provide image
    • Blog images → Website repo
    • Social/Ads images → Separate folder
  4. API Ready: Output structures match Google Ads and Meta Ads API schemas for future integration.

  5. Data Services Optional: Skips unconfigured services (GA4, GSC, DataForSEO, Umami).

  6. Per-Project Context: Each website has its own context/ folder with brand voice, keywords, guidelines.


🔄 Integration with Other Skills

  • website-creator: Blog posts published to Astro content collections
  • seo-analyzers: Quality scoring and Thai language analysis
  • seo-data: Performance data for content optimization
  • seo-context: Context file management

Success Criteria

  • Content generated for all selected channels
  • Thai language processing accurate (word count, keyword density)
  • Product images found or user asked to provide
  • Blog posts published to Astro (if enabled)
  • Git commit + push successful (triggers auto-deploy)
  • Output structures API-ready for future integration
  • Quality scores ≥ 70/100 for all content

Use this skill when you need to create multi-channel marketing content from a single topic with full Thai language support and automatic image handling.