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
This commit is contained in:
275
skills/mm-blog-categories/SKILL.md
Normal file
275
skills/mm-blog-categories/SKILL.md
Normal file
@@ -0,0 +1,275 @@
|
||||
---
|
||||
name: mm-blog-categories
|
||||
description: >
|
||||
Fetch, cache, and manage blog categories from Astro or WordPress websites.
|
||||
Caches categories in clients-config.json to avoid repeated API calls.
|
||||
Used by mm-content-writer to ensure articles use existing categories.
|
||||
Use when: "ดึง categories", "blog categories", "fetch categories",
|
||||
"refresh categories", "categories ของเว็บ", "update category cache".
|
||||
---
|
||||
|
||||
# mm-blog-categories
|
||||
|
||||
Manages blog categories for moreminimore content pipeline.
|
||||
Fetches from website (Astro or WordPress), caches in clients-config.json.
|
||||
|
||||
## When This Must Trigger
|
||||
|
||||
- "ดึง categories", "fetch categories", "blog categories"
|
||||
- "refresh categories", "update category cache"
|
||||
- "categories ของเว็บ", "มี category อะไรบ้าง"
|
||||
- Called internally by mm-content-writer before assigning categories
|
||||
|
||||
## Config Structure
|
||||
|
||||
Categories are cached per-client in `~/vault/99_System/clients-config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "client-id",
|
||||
"website": {
|
||||
"type": "wordpress",
|
||||
"url": "https://example.com",
|
||||
"categories": [
|
||||
{ "id": 1, "name": "Technology", "slug": "technology", "count": 12 },
|
||||
{ "id": 2, "name": "Marketing", "slug": "marketing", "count": 8 }
|
||||
],
|
||||
"categories_cached_at": "2026-06-30T10:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For Astro:
|
||||
```json
|
||||
{
|
||||
"website": {
|
||||
"type": "astro",
|
||||
"repo": "git.moreminimore.com/kunthawat/site",
|
||||
"categories": [
|
||||
{ "name": "Technology", "slug": "technology" },
|
||||
{ "name": "Marketing", "slug": "marketing" }
|
||||
],
|
||||
"categories_cached_at": "2026-06-30T10:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Process
|
||||
|
||||
### Step 1: Determine client
|
||||
|
||||
If called directly, ask which client:
|
||||
|
||||
```
|
||||
ดึง categories ของเว็บไหน?
|
||||
|
||||
1. [client-1] — [website type]: [url]
|
||||
2. [client-2] — [website type]: [url]
|
||||
...
|
||||
```
|
||||
|
||||
If called by mm-content-writer, use the `related_client` from the article.
|
||||
|
||||
### Step 2: Check cache
|
||||
|
||||
Read `clients-config.json`. Check if `categories` and `categories_cached_at` exist.
|
||||
|
||||
**Cache is fresh** if `categories_cached_at` is less than 7 days old:
|
||||
|
||||
```python
|
||||
from datetime import datetime, timezone
|
||||
|
||||
cached_at = client['website'].get('categories_cached_at')
|
||||
if cached_at:
|
||||
cached_dt = datetime.fromisoformat(cached_at.replace('Z', '+00:00'))
|
||||
age_days = (datetime.now(timezone.utc) - cached_dt).days
|
||||
if age_days < 7:
|
||||
# Cache is fresh, use it
|
||||
return client['website']['categories']
|
||||
```
|
||||
|
||||
**Cache is stale or missing** → fetch from website.
|
||||
|
||||
### Step 3a: Fetch from WordPress
|
||||
|
||||
```bash
|
||||
WP_URL="<url from config>"
|
||||
```
|
||||
|
||||
```bash
|
||||
# Fetch all categories
|
||||
curl -s "$WP_URL/wp-json/wp/v2/categories?per_page=100&_fields=id,name,slug,count" | python3 -c "
|
||||
import json, sys
|
||||
cats = json.load(sys.stdin)
|
||||
for c in cats:
|
||||
print(json.dumps({
|
||||
'id': c['id'],
|
||||
'name': c['name'],
|
||||
'slug': c['slug'],
|
||||
'count': c.get('count', 0)
|
||||
}))
|
||||
"
|
||||
```
|
||||
|
||||
Handle pagination if > 100 categories:
|
||||
```bash
|
||||
# Check total pages from response headers
|
||||
curl -sI "$WP_URL/wp-json/wp/v2/categories?per_page=100" | grep -i 'x-wp-totalpages'
|
||||
```
|
||||
|
||||
### Step 3b: Fetch from Astro
|
||||
|
||||
Astro categories are defined in content frontmatter. Need to scan the repo:
|
||||
|
||||
```bash
|
||||
REPO="<repo from config>"
|
||||
CLONE_DIR="/tmp/astro-categories-$(date +%s)"
|
||||
```
|
||||
|
||||
1. Clone or pull the repo:
|
||||
```bash
|
||||
git clone --depth 1 "https://$REPO" "$CLONE_DIR" 2>/dev/null || \
|
||||
git clone --depth 1 "git@$REPO" "$CLONE_DIR"
|
||||
```
|
||||
|
||||
2. Scan blog posts for unique categories:
|
||||
```python
|
||||
import os, re, yaml
|
||||
|
||||
blog_dir = os.path.join(CLONE_DIR, "src/content/blog")
|
||||
categories = set()
|
||||
|
||||
for root, dirs, files in os.walk(blog_dir):
|
||||
for f in files:
|
||||
if not f.endswith(('.md', '.mdx')):
|
||||
continue
|
||||
path = os.path.join(root, f)
|
||||
content = open(path).read()
|
||||
# Extract frontmatter
|
||||
match = re.match(r'^---\s*\n(.*?)\n---', content, re.DOTALL)
|
||||
if match:
|
||||
try:
|
||||
fm = yaml.safe_load(match.group(1))
|
||||
if fm and 'categories' in fm:
|
||||
cats = fm['categories']
|
||||
if isinstance(cats, list):
|
||||
categories.update(cats)
|
||||
elif isinstance(cats, str):
|
||||
categories.add(cats)
|
||||
if fm and 'category' in fm:
|
||||
categories.add(fm['category'])
|
||||
except:
|
||||
continue
|
||||
|
||||
# Convert to list
|
||||
result = [{'name': c, 'slug': c.lower().replace(' ', '-')} for c in sorted(categories)]
|
||||
```
|
||||
|
||||
3. Clean up:
|
||||
```bash
|
||||
rm -rf "$CLONE_DIR"
|
||||
```
|
||||
|
||||
### Step 4: Update cache
|
||||
|
||||
Write fetched categories back to `clients-config.json`:
|
||||
|
||||
```python
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
|
||||
config_path = os.path.expanduser("~/vault/99_System/clients-config.json")
|
||||
config = json.load(open(config_path))
|
||||
|
||||
for client in config['clients']:
|
||||
if client['id'] == client_id:
|
||||
client['website']['categories'] = fetched_categories
|
||||
client['website']['categories_cached_at'] = datetime.now(timezone.utc).isoformat() + 'Z'
|
||||
break
|
||||
|
||||
json.dump(config, open(config_path, 'w'), indent=2, ensure_ascii=False)
|
||||
```
|
||||
|
||||
### Step 5: Present
|
||||
|
||||
```
|
||||
📂 Categories สำหรับ [client_name] ([website_type]):
|
||||
|
||||
| # | Category | Slug | Posts |
|
||||
|---|----------|------|-------|
|
||||
| 1 | Technology | technology | 12 |
|
||||
| 2 | Marketing | marketing | 8 |
|
||||
| 3 | Business | business | 5 |
|
||||
|
||||
✅ Cache updated: categories_cached_at = [timestamp]
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### Show categories (from cache or fetch)
|
||||
```
|
||||
mm categories [client-id]
|
||||
```
|
||||
|
||||
### Force refresh
|
||||
```
|
||||
mm categories refresh [client-id]
|
||||
```
|
||||
|
||||
### Add new category (to website)
|
||||
```
|
||||
mm categories add [client-id] "[category name]"
|
||||
```
|
||||
- WordPress: POST to `/wp-json/wp/v2/categories`
|
||||
- Astro: manual (add to frontmatter convention doc)
|
||||
|
||||
## Integration with mm-content-writer
|
||||
|
||||
When mm-content-writer assigns categories to an article:
|
||||
|
||||
1. Call mm-blog-categories to get categories (uses cache if fresh)
|
||||
2. Present categories to user or auto-select based on article topic
|
||||
3. If user wants a new category:
|
||||
- WordPress: create via API, update cache
|
||||
- Astro: add to convention, update cache
|
||||
4. Use exact category name/slug from cache (no guessing)
|
||||
|
||||
## Error Handling
|
||||
|
||||
If WordPress API fails:
|
||||
```
|
||||
❌ ไม่สามารถดึง categories จาก WordPress ได้
|
||||
|
||||
สาเหตุที่เป็นไปได้:
|
||||
- REST API ถูกปิดใช้งาน → ตรวจสอบ wp-admin → Settings → Permalinks
|
||||
- URL ไม่ถูกต้อง → ตรวจสอบ clients-config.json
|
||||
- Authentication required → บางเว็บต้องใช้ Application Password
|
||||
|
||||
ต้องการ:
|
||||
1. ลองใหม่
|
||||
2. เพิ่ม categories ด้วยตนเอง
|
||||
3. ข้าม
|
||||
```
|
||||
|
||||
If Astro clone fails:
|
||||
```
|
||||
❌ ไม่สามารถ clone Astro repo ได้
|
||||
|
||||
สาเหตุที่เป็นไปได้:
|
||||
- Git credentials ไม่ถูกต้อง
|
||||
- Repo URL ไม่ถูกต้อง → ตรวจสอบ clients-config.json
|
||||
- Network error
|
||||
|
||||
ต้องการ:
|
||||
1. ลองใหม่
|
||||
2. เพิ่ม categories ด้วยตนเอง
|
||||
3. ข้าม
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Cache expires after 7 days (configurable)
|
||||
- WordPress categories include post count; Astro categories don't
|
||||
- For Astro, categories are inferred from frontmatter — there's no central registry
|
||||
- The skill handles both website types transparently based on `website.type`
|
||||
- mm-content-writer should call this skill before assigning categories
|
||||
Reference in New Issue
Block a user