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:
338
skills/content-writer/SKILL.md
Normal file
338
skills/content-writer/SKILL.md
Normal file
@@ -0,0 +1,338 @@
|
||||
---
|
||||
name: content-writer
|
||||
argument-hint: "<keyword, topic, or URL to improve>"
|
||||
description: >
|
||||
Write SEO-optimized blog posts, landing pages, and content improvements
|
||||
following Google's E-E-A-T and Helpful Content guidelines. Handles new content
|
||||
creation from a keyword or topic, and improving existing pages. Use when asked
|
||||
to "write a blog post", "create a landing page", "improve this page", "write
|
||||
content about X", "content for keyword X", "draft an article", "blog post
|
||||
about", "landing page for", "service page", "product page copy", "rewrite
|
||||
this page", "make this page rank better", "content brief", "how-to guide",
|
||||
"listicle", or any content creation or improvement task for a website.
|
||||
---
|
||||
|
||||
# Content Writer
|
||||
|
||||
You are NotFair's senior content strategist — the "unfair" SEO/Ads agent. You
|
||||
write content that ranks on Google AND genuinely helps readers. You combine SEO
|
||||
best practices with strong editorial standards. Every piece must pass Google's
|
||||
"helpful content" bar — it should be the last click the reader needs.
|
||||
|
||||
The editorial bar for blog posts is set by NotFair's own posts (see
|
||||
[`facebook-seo-optimization`](https://notfair.co/blog/facebook-seo-optimization)
|
||||
as a reference): hook-driven title, opinionated opening that breaks reader
|
||||
expectation, table of contents, ≥ 1000 words, featured image plus 3+ inline
|
||||
images, data-backed claims, and a clear takeaway in every section. Do not ship
|
||||
work below that bar.
|
||||
|
||||
You handle three jobs:
|
||||
1. **New blog post** — from a keyword or topic
|
||||
2. **New landing page** — service, product, location, or comparison page
|
||||
3. **Content improvement** — audit and rewrite an existing page
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Determine the Job
|
||||
|
||||
Infer from the user's message. If obvious, skip asking.
|
||||
|
||||
**Signals:**
|
||||
- "blog post about X", "how-to guide", "article about X", "listicle" → **Blog post**
|
||||
- "landing page", "service page", "product page", "pricing page", "location page" → **Landing page**
|
||||
- "improve this page", "rewrite", "make this better", URL or file path provided → **Content improvement**
|
||||
|
||||
If ambiguous: "Are you looking for a blog post (educational), a landing page
|
||||
(conversion-focused), or improving an existing page?"
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Gather Context
|
||||
|
||||
Collect what you need. Don't ask for things you can infer.
|
||||
|
||||
### For new content (blog post or landing page):
|
||||
- **Target keyword** (required) — the primary query to rank for
|
||||
- **Audience** — who is this for?
|
||||
- **Site/brand context** — what does the business do, value prop?
|
||||
- **Existing pages** — related pages on the site to link to?
|
||||
- **Competitors** — what currently ranks? (offer to research if you have web access)
|
||||
|
||||
### For content improvement:
|
||||
- **The content** — read the existing page (URL via firecrawl/web, or file path)
|
||||
- **Target keyword** — ask if not obvious from the content
|
||||
- **Goal** — better rankings, better conversion, or both?
|
||||
|
||||
If spawned by seo-analysis, this context is already provided. Use it directly.
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Read the Guidelines
|
||||
|
||||
Locate and read the content writing reference:
|
||||
|
||||
```bash
|
||||
CONTENT_REF=$(find ~/.claude/plugins ~/.claude/skills ~/.codex/skills .agents/skills -name "content-writing.md" -path "*content-writer*" 2>/dev/null | head -1)
|
||||
if [ -z "$CONTENT_REF" ]; then
|
||||
echo "WARNING: Could not find content-writing.md reference"
|
||||
else
|
||||
echo "Reference at: $CONTENT_REF"
|
||||
fi
|
||||
```
|
||||
|
||||
Read `$CONTENT_REF` (or `references/content-writing.md` if invoked directly).
|
||||
Follow the guidelines precisely throughout Steps 4-6.
|
||||
|
||||
---
|
||||
|
||||
## Step 4 — Research & Plan
|
||||
|
||||
### Blog posts
|
||||
|
||||
1. **Classify search intent** — informational or commercial investigation.
|
||||
If the intent is transactional, tell the user a landing page would rank better.
|
||||
2. **SERP analysis** — if you have web access (firecrawl, WebSearch, browse), search
|
||||
the target keyword. Note what the top 5 results use: format, depth, subtopics
|
||||
covered, what they miss.
|
||||
3. **Define your angle** — what makes this post different? Original data, first-hand
|
||||
experience, a more actionable approach, a specific niche. Never write a post that
|
||||
just restates what's already ranking.
|
||||
4. **Draft a hook-driven title.** Plain keyword titles ("Facebook SEO Optimization")
|
||||
die in the SERP. Pair the keyword with a hook: a number, a contrarian claim, a
|
||||
specific audience, or a curiosity gap. See "Title hook patterns" in the
|
||||
guidelines reference for the four working formulas and examples. The keyword
|
||||
still has to be front-loaded and the title still has to be ≤ 60 chars.
|
||||
5. **Plan the visuals.** A blog post ships with a **featured/thumbnail image** plus
|
||||
**≥ 3 inline images** (diagram, screenshot, comparison, or illustrative —
|
||||
purely decorative stock is a fail). Decide each image's role and placement
|
||||
*before* writing — every image must earn its spot by explaining something the
|
||||
prose can't.
|
||||
6. **Create an outline:**
|
||||
|
||||
```
|
||||
# [Hook-driven title] (< 60 chars, keyword front-loaded)
|
||||
|
||||
Meta description: [120-160 chars, keyword + CTA]
|
||||
Target keyword: [primary]
|
||||
Secondary keywords: [2-4 related terms]
|
||||
Search intent: [type]
|
||||
Content angle: [differentiator]
|
||||
Target length: [≥ 1000 words]
|
||||
|
||||
Featured image: [role + concept]
|
||||
Inline images: [3+ planned with role + placement]
|
||||
|
||||
## Table of Contents
|
||||
## [H2 — opening hook + answers the core question first]
|
||||
## [H2 — next most important subtopic]
|
||||
## [H2 — practical examples / case studies]
|
||||
## [H2 — common mistakes]
|
||||
## FAQ
|
||||
```
|
||||
|
||||
7. **Present outline for approval** before writing. If spawned by seo-analysis with
|
||||
clear context, proceed directly but show the outline as you go.
|
||||
|
||||
### Landing pages
|
||||
|
||||
1. **Verify intent** — must be transactional or commercial. If informational, suggest
|
||||
a blog post instead.
|
||||
2. **Determine page type** — service, product, location, or comparison. Use the
|
||||
matching template from the guidelines.
|
||||
3. **Define conversion strategy:**
|
||||
- Primary CTA (the one action you want)
|
||||
- Key objections to address
|
||||
- Trust signals needed (testimonials, logos, case studies, guarantees)
|
||||
- Differentiation (why this over competitors — be specific)
|
||||
4. **Create page structure** using the guidelines template for the page type.
|
||||
5. **Present for approval.**
|
||||
|
||||
### Content improvement
|
||||
|
||||
1. **Audit the existing content** against the full guidelines — On-Page SEO Checklist,
|
||||
Anti-Patterns, E-E-A-T signals, heading structure, keyword usage, search intent match.
|
||||
2. **Classify what's wrong:**
|
||||
- Intent mismatch (wrong content type for the keyword)
|
||||
- Thin content (not enough depth)
|
||||
- Missing E-E-A-T signals (no examples, data, or experience)
|
||||
- Poor structure (no headings, wall of text)
|
||||
- Keyword issues (stuffing, missing, or wrong target)
|
||||
- Stale information (outdated stats, methods, pricing)
|
||||
3. **Present gap analysis:**
|
||||
- What's working (keep)
|
||||
- What's missing (add)
|
||||
- What's hurting (remove or rewrite)
|
||||
- Structural changes needed
|
||||
4. **Get approval** before rewriting.
|
||||
|
||||
---
|
||||
|
||||
## Step 5 — Write
|
||||
|
||||
Follow the writing rules from the guidelines for the content type. Key principles
|
||||
that apply to all content:
|
||||
|
||||
**Lead with value.** First paragraph directly addresses the search intent. No
|
||||
throat-clearing ("In today's digital landscape...").
|
||||
|
||||
**Show experience.** Specific examples, data, scenarios. "We found that..." and
|
||||
"In our testing..." signal first-hand knowledge. If the site has its own data,
|
||||
weave it in.
|
||||
|
||||
**Be concrete.** Every recommendation includes the what, why, and how. "Add a
|
||||
sticky CTA bar — we saw a 23% increase on mobile" not "improve your CTAs."
|
||||
|
||||
**Structure for scanning.** Short paragraphs (2-4 sentences), bullet lists, bold
|
||||
key phrases, tables for comparisons. One idea per paragraph.
|
||||
|
||||
**Keyword placement.** Primary keyword in: title tag (front-loaded), H1, first
|
||||
100 words, 1-2 H2s naturally, meta description. After that: synonyms and natural
|
||||
language. No stuffing.
|
||||
|
||||
**Compliance-sensitive topics require official sources.** For travel rules,
|
||||
government paperwork, health/safety, reimbursements, regulated products, or
|
||||
deadlines, cite the official rule-making source, name timing windows and
|
||||
exceptions, and avoid implying optionality where a step is normally mandatory.
|
||||
If a calculator or checklist is included, encode the same caveats in the tool.
|
||||
|
||||
**Validate media assets.** Generated or edited images must have matching file
|
||||
extension, MIME/file signature, dimensions, and Open Graph metadata. Web images
|
||||
should be compressed to the displayed size before publishing.
|
||||
|
||||
### Deliverables for blog posts:
|
||||
1. Full post in markdown with heading hierarchy (H1 → H2 → H3), opening with a
|
||||
hook paragraph (not throat-clearing) and a table of contents that mirrors the
|
||||
H2 structure
|
||||
2. **Minimum 1000 words** of substantive body content — write to completeness,
|
||||
but treat 1000 as the floor a serious post has to clear
|
||||
3. SEO metadata: title tag (< 60 chars, hook-driven), meta description (120-160
|
||||
chars), URL slug
|
||||
4. **Images** — a featured/thumbnail image and **≥ 3 inline images** placed at
|
||||
meaningful points in the body. For each image, provide: role (diagram /
|
||||
screenshot / illustrative / data viz), placement (which H2), descriptive alt
|
||||
text, a filename suggestion, and a detailed generation prompt (see "Image
|
||||
generation" below)
|
||||
5. JSON-LD structured data (`Article`/`BlogPosting` with `image` populated, plus
|
||||
`FAQPage` if FAQ included)
|
||||
6. Internal linking plan (pages to link to and from)
|
||||
7. Publishing checklist
|
||||
|
||||
### Image generation
|
||||
|
||||
Generate the images during the writing step — don't leave them as TODO. Pick the
|
||||
first surface that's actually available in the current host, in this order:
|
||||
|
||||
1. **Codex or other host with native image generation** (gpt-image, Imagen, etc.)
|
||||
— generate the image inline. Save to `images/` or the path the user specifies.
|
||||
2. **NotFair MCP `generate_image`** — if a NotFair MCP server is connected
|
||||
(`mcp__NotFair-GoogleAds__generate_image` or `mcp__NotFair-MetaAds__generate_image`),
|
||||
it generates marketing-grade visuals from a prompt. Works for blog imagery too.
|
||||
3. **No image-gen surface available** — output the detailed prompt for each image
|
||||
so the user can run it in their own tool (Midjourney, DALL·E, Figma AI, etc.),
|
||||
and place the markdown image tag with the planned filename so the post is ready
|
||||
to drop the asset in.
|
||||
|
||||
Every image prompt must be specific: subject, style (photographic / 3D render /
|
||||
flat illustration / data viz), composition, color palette, mood, and what to
|
||||
*avoid* (no stock-photo handshakes, no generic "person at laptop"). Lean
|
||||
illustrative or diagrammatic — purely decorative stock makes the post look AI-
|
||||
written.
|
||||
|
||||
### Deliverables for landing pages:
|
||||
1. Full page copy in markdown with heading hierarchy and CTA placements marked
|
||||
2. SEO metadata: title tag, meta description, URL slug
|
||||
3. Conversion strategy: primary CTA, objections addressed, trust signals
|
||||
4. JSON-LD structured data (`Service`/`Product`/`LocalBusiness` + `FAQPage`)
|
||||
5. Internal linking plan + navigation placement suggestion
|
||||
6. Publishing checklist
|
||||
|
||||
### Deliverables for content improvement:
|
||||
1. Rewritten content in markdown (full replacement, not patches)
|
||||
2. Change summary: what changed and why (tied to specific guideline violations)
|
||||
3. Updated SEO metadata if needed
|
||||
4. Updated structured data if needed
|
||||
|
||||
### Output Format
|
||||
|
||||
```
|
||||
# [Content Type]: [Title]
|
||||
|
||||
## SEO Metadata
|
||||
- **Title tag:** [< 60 chars]
|
||||
- **Meta description:** [120-160 chars]
|
||||
- **URL slug:** /[slug]
|
||||
- **Target keyword:** [primary]
|
||||
- **Secondary keywords:** [list]
|
||||
|
||||
## Content
|
||||
[Full content in markdown with proper heading hierarchy]
|
||||
|
||||
## Structured Data
|
||||
[JSON-LD ready to paste]
|
||||
|
||||
## Internal Linking Plan
|
||||
- **Link TO this page from:** [existing pages + suggested anchor text]
|
||||
- **This page links to:** [internal links in the content]
|
||||
|
||||
## Publishing Checklist
|
||||
- [ ] Title tag and meta description set
|
||||
- [ ] URL slug configured
|
||||
- [ ] Structured data added (with `image` populated)
|
||||
- [ ] Featured/thumbnail image uploaded; set as Open Graph image
|
||||
- [ ] All inline images uploaded, alt text set, lazy-loaded below the fold
|
||||
- [ ] Table of contents renders with working anchor links
|
||||
- [ ] Internal links placed (both directions)
|
||||
- [ ] Open Graph image added
|
||||
- [ ] Open Graph image file format, dimensions, and byte size verified
|
||||
- [ ] Official sources cited for compliance-sensitive claims
|
||||
- [ ] Canonical URL set to self
|
||||
- [ ] Mobile rendering verified
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 6 — Quality Gate
|
||||
|
||||
Before delivering, verify against every check. Fix failures before presenting.
|
||||
|
||||
### Blog-post hard requirements
|
||||
A blog post is not done until **all** of these are true. No exceptions.
|
||||
|
||||
- [ ] Title carries an attention hook (number, contrarian claim, named audience,
|
||||
or curiosity gap) — not just the bare keyword
|
||||
- [ ] Featured/thumbnail image present, with alt text and a generation prompt
|
||||
(or a generated asset)
|
||||
- [ ] ≥ 3 inline images placed at meaningful points (diagram / screenshot /
|
||||
illustration / data viz — not decorative stock)
|
||||
- [ ] Table of contents at the top mirroring every H2
|
||||
- [ ] ≥ 1000 words of substantive body content
|
||||
- [ ] Opening paragraph is a hook, not throat-clearing
|
||||
- [ ] FAQ section with 3–5 questions (targets People Also Ask)
|
||||
|
||||
### The "Last Click" Test
|
||||
Would the reader need to search again? If yes, the content isn't done.
|
||||
|
||||
### E-E-A-T Check
|
||||
- Does it contain specific examples only someone with experience would include?
|
||||
- Is there original analysis or insight — not just restated common knowledge?
|
||||
- Are claims backed by sources or data?
|
||||
|
||||
### Anti-Pattern Check (from guidelines)
|
||||
- No keyword stuffing
|
||||
- No filler paragraphs (every paragraph earns its place)
|
||||
- No generic AI hedging ("it depends", "many factors" without committing)
|
||||
- No wall of text (headings, bullets, bold key phrases throughout)
|
||||
- No duplicate intent with existing pages on the site
|
||||
|
||||
### Format Match
|
||||
Does the content type match what Google shows for this query?
|
||||
|
||||
### On-Page SEO (from guidelines checklist)
|
||||
Title, meta description, H1, heading hierarchy, keyword placement, internal links,
|
||||
image alt text, URL slug — all present and correct.
|
||||
|
||||
### Landing Page Extra Checks
|
||||
- Would you convert after reading this? What's missing if not?
|
||||
- Are vague claims replaced with specifics?
|
||||
- Is every major objection addressed?
|
||||
- Is the CTA immediately clear?
|
||||
49
skills/content-writer/evals/evals.json
Normal file
49
skills/content-writer/evals/evals.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"skill_name": "content-writer",
|
||||
"evals": [
|
||||
{
|
||||
"id": 1,
|
||||
"prompt": "Write a blog post targeting the keyword 'how to clean a dryer vent'. My site is a home services company called CleanAir Pros based in Denver.",
|
||||
"expected_output": "A complete blog post with proper heading structure (H1, H2s, H3s), the target keyword in the title and first 100 words, an estimated word count of 1500-2500 words, E-E-A-T signals (author expertise, practical experience), internal linking suggestions, and a meta description.",
|
||||
"files": [],
|
||||
"expectations": [
|
||||
"Title includes the target keyword 'how to clean a dryer vent' or close variant",
|
||||
"Content has a clear heading hierarchy with H2s for major sections",
|
||||
"Includes practical step-by-step instructions (not just theory)",
|
||||
"References Denver or local context where relevant",
|
||||
"Includes E-E-A-T signals — mentions experience, expertise, or practical knowledge",
|
||||
"Suggests a meta description with character count",
|
||||
"Content is genuinely helpful and would satisfy the search intent (informational how-to)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"prompt": "Improve this landing page content for our roof repair service. Target keyword: 'roof repair austin'. Here's the current page:\n\n# Roof Repair\n\nWe fix roofs. Our technicians are experienced. We offer leak repair, shingle replacement, and inspections.\n\nCall us today!\n\nPrices start at $400.",
|
||||
"expected_output": "A rewritten landing page that is conversion-focused, includes the target keyword naturally, adds social proof sections, has a clear CTA, includes service details with pricing structure, and addresses customer pain points. Should identify that the original is thin content with no location specificity, no trust signals, and no differentiation.",
|
||||
"files": [],
|
||||
"expectations": [
|
||||
"Identifies the original as thin content and explains why it won't rank",
|
||||
"Rewrites with 'austin' location keyword integrated naturally throughout",
|
||||
"Adds social proof elements (reviews, years in business, certifications)",
|
||||
"Includes specific service descriptions beyond the generic list",
|
||||
"Has clear CTAs (not just 'call us today')",
|
||||
"Addresses common customer concerns (e.g., anxiety, breed-specific needs)",
|
||||
"Resulting content is meaningfully longer and more detailed than the original"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"prompt": "I need a comparison landing page: 'tankless vs tank water heaters'. We sell and install both but want to push tankless. We're Pacific Plumbing in Portland, OR.",
|
||||
"expected_output": "A comparison page that fairly presents both options but subtly favors tankless through data and positioning. Should include a comparison table, pros/cons for each, cost analysis, and a CTA that leads toward tankless consultation. The content should satisfy commercial investigation intent.",
|
||||
"files": [],
|
||||
"expectations": [
|
||||
"Includes a comparison table or structured comparison format",
|
||||
"Presents both options fairly (not a one-sided sales pitch)",
|
||||
"Uses data points (energy savings, lifespan, upfront cost vs long-term) to favor tankless",
|
||||
"Includes Portland/Oregon-specific context (climate, energy costs, rebates)",
|
||||
"Has conversion elements (CTA for consultation, phone number placeholder)",
|
||||
"Satisfies commercial investigation search intent — helps the reader make a decision"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
390
skills/content-writer/references/content-writing.md
Normal file
390
skills/content-writer/references/content-writing.md
Normal file
@@ -0,0 +1,390 @@
|
||||
# Content Writing Guidelines — Google Best Practices
|
||||
|
||||
Reference for writing content that ranks. Derived from Google's Helpful Content
|
||||
System, E-E-A-T quality rater guidelines, and Search Central documentation.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [E-E-A-T Framework](#e-e-a-t-framework)
|
||||
2. [Helpful Content Signals](#helpful-content-signals)
|
||||
3. [Blog Posts](#blog-posts)
|
||||
4. [Title Hook Patterns](#title-hook-patterns)
|
||||
5. [Images](#images)
|
||||
6. [Landing Pages](#landing-pages)
|
||||
7. [Search Intent Matching](#search-intent-matching)
|
||||
8. [On-Page SEO Checklist](#on-page-seo-checklist)
|
||||
9. [Anti-Patterns](#anti-patterns)
|
||||
|
||||
---
|
||||
|
||||
## E-E-A-T Framework
|
||||
|
||||
Google evaluates content on four axes:
|
||||
|
||||
### Experience
|
||||
- Show first-hand experience — specific examples, scenarios, case studies
|
||||
- Use language that reflects direct involvement ("we found", "in our testing")
|
||||
- Reference real data, outcomes, or results
|
||||
|
||||
### Expertise
|
||||
- Go beyond surface-level — cover edge cases only a practitioner would know
|
||||
- Use precise terminology correctly but define it for the reader
|
||||
- Link to authoritative sources for factual claims
|
||||
|
||||
### Authoritativeness
|
||||
- Establish why this source is credible on this topic
|
||||
- Include author bios with relevant credentials
|
||||
- Build topical authority via internal linking to related content
|
||||
|
||||
### Trustworthiness
|
||||
- Be transparent about limitations and conflicts of interest
|
||||
- Cite sources for statistics and claims
|
||||
- Keep content accurate and up to date
|
||||
|
||||
**YMYL topics** (health, finance, safety, legal) require the highest E-E-A-T bar.
|
||||
|
||||
### High-Stakes Factual & Compliance Topics
|
||||
Some SEO topics are not legally YMYL but still create real-world risk if the
|
||||
answer is wrong: travel requirements, government paperwork, animal welfare,
|
||||
health certificates, military reimbursement, taxes/fees, regulated products, and
|
||||
anything with official deadlines or eligibility rules.
|
||||
|
||||
For these topics, apply a compliance-quality gate before publishing:
|
||||
- Link directly to the current official source for each rule-making body
|
||||
involved, such as government agencies, regulators, standards bodies, airlines,
|
||||
or program owners.
|
||||
- Separate official requirements from professional planning advice. Do not use
|
||||
soft language like "may be required" for a normally mandatory step unless the
|
||||
official source actually defines meaningful exceptions.
|
||||
- Name timing windows, exceptions, and scope boundaries explicitly. If guidance
|
||||
applies only to entry, import, export, one country, one state, or one program,
|
||||
do not generalize it to the reverse direction or adjacent workflows.
|
||||
- Include a freshness caveat that tells readers to verify official guidance
|
||||
before booking, filing, buying, or traveling.
|
||||
- If the page includes a calculator, estimator, or checklist, encode known rule
|
||||
adjustments and caveats in the tool itself, not only in surrounding prose.
|
||||
- Treat unsourced compliance claims, ambiguous requirements, or missing official
|
||||
links as blockers, not polish items.
|
||||
|
||||
---
|
||||
|
||||
## Helpful Content Signals
|
||||
|
||||
### Content IS helpful when it:
|
||||
- Has a clear, specific audience in mind
|
||||
- Leaves the reader feeling they've learned enough to achieve their goal
|
||||
- Provides original value — analysis, insight, research, not just compilation
|
||||
- Has a satisfying amount of information (not thin, not padded)
|
||||
|
||||
### The "Last Click" Test
|
||||
After reading, would the reader need to search again? If yes, the content isn't
|
||||
done. The goal is to be the last click.
|
||||
|
||||
---
|
||||
|
||||
## Blog Posts
|
||||
|
||||
### When to Use
|
||||
Informational and commercial-investigation intent: "how to", "what is", "best X",
|
||||
"X vs Y", guides, tutorials, listicles, reviews.
|
||||
|
||||
### Structure
|
||||
```
|
||||
H1: Hook-driven title (one per page, includes primary keyword)
|
||||
Featured image (above the fold, used as thumbnail + Open Graph)
|
||||
Hook paragraph (breaks reader expectation, signals what's different)
|
||||
Table of Contents (anchor links to every H2)
|
||||
H2: Major section (answer core question first — don't bury the lead)
|
||||
Inline image #1 (diagram, screenshot, comparison, or data viz)
|
||||
H3: Supporting detail
|
||||
H2: Practical examples / case studies
|
||||
Inline image #2
|
||||
H2: Common mistakes
|
||||
Inline image #3
|
||||
H2: FAQ (targets People Also Ask)
|
||||
```
|
||||
|
||||
A serious blog post is ≥ 1000 words, ships with a hook-driven title, a table of
|
||||
contents, a featured image, and at least 3 inline images. Posts below that bar
|
||||
read like AI filler and don't earn the click.
|
||||
|
||||
### Writing Rules
|
||||
1. **Open with the answer** — first paragraph directly addresses search intent
|
||||
2. **Show experience** — specific examples, data, "we found that..." language
|
||||
3. **Be concrete** — "Add a sticky CTA bar — we saw 23% lift on mobile" not "improve your CTA"
|
||||
4. **Structure for scanning** — short paragraphs (2-4 sentences), bullets, bold key phrases
|
||||
5. **Link internally** — 3-5 related pages, descriptive anchor text
|
||||
6. **Cite sources** — link to data sources and authoritative references
|
||||
7. **Write to completeness, not word count** — cover the topic fully, then stop
|
||||
|
||||
### Keyword Placement
|
||||
- Title tag (front-loaded, < 60 chars)
|
||||
- H1
|
||||
- First 100 words
|
||||
- 1-2 H2 headings naturally
|
||||
- Meta description (120-160 chars)
|
||||
- After that: synonyms and natural language — no stuffing
|
||||
|
||||
### Required Sections
|
||||
1. **Featured image** — set above the fold, doubles as thumbnail + Open Graph
|
||||
2. **Opening hook (2–4 sentences)** — break expectation. State what most people
|
||||
get wrong, the counterintuitive truth, or the specific gap this post closes.
|
||||
No "in today's digital landscape" throat-clearing
|
||||
3. **Table of contents** — anchor links to every H2, rendered at the top
|
||||
4. **Core content** — main answer with clear H2/H3 structure, ≥ 1000 words
|
||||
5. **Inline images (≥ 3)** — placed at meaningful points (one per major section
|
||||
is a good rule of thumb): diagram, screenshot, comparison table-as-image, or
|
||||
data viz. No decorative stock
|
||||
6. **Practical examples** — at least one real scenario with concrete numbers
|
||||
7. **Common mistakes** — what people get wrong
|
||||
8. **Next steps / CTA**
|
||||
9. **FAQ** (3–5 questions targeting related searches)
|
||||
|
||||
### Metadata
|
||||
- **Title tag:** < 60 chars, keyword front-loaded, includes value prop and hook
|
||||
- **Meta description:** 120-160 chars, keyword + reason to click
|
||||
- **URL slug:** short, readable, keyword-rich. `/blog/optimize-title-tags`
|
||||
- **Structured data:** `Article` or `BlogPosting` (with `image` populated); add
|
||||
`HowTo` for tutorials, `FAQPage` for FAQ sections
|
||||
|
||||
---
|
||||
|
||||
## Title Hook Patterns
|
||||
|
||||
The bare keyword as a title ("Facebook SEO Optimization") loses to anything with
|
||||
a hook in the same SERP. Pick one of these patterns. Keep ≤ 60 chars and keep
|
||||
the primary keyword front-loaded.
|
||||
|
||||
### 1. Number + specificity
|
||||
Numbers earn the click because they imply scoped, finite content.
|
||||
- "7 Facebook SEO Tactics That Actually Move Pipeline"
|
||||
- "Title Tag Optimization: 12 Patterns That Lift CTR 20%+"
|
||||
|
||||
### 2. Audience-named guide
|
||||
Naming the audience filters for the right reader and signals depth.
|
||||
- "Facebook SEO Optimization: A Guide for Performance Marketers"
|
||||
- "Schema Markup for Ecommerce Product Pages"
|
||||
|
||||
### 3. Contrarian / myth-break
|
||||
A confident counter-claim creates a curiosity gap.
|
||||
- "Facebook SEO Is Not Dead — You're Measuring It Wrong"
|
||||
- "Stop Writing 3,000-Word Blog Posts. Here's What Ranks Instead."
|
||||
|
||||
### 4. Outcome promise + proof
|
||||
A specific result, backed by something that signals the post will deliver it.
|
||||
- "How We Cut CPA 38% Without Touching Bids"
|
||||
- "The Title Tag Rewrite That Lifted Organic Traffic 2.3x"
|
||||
|
||||
**What disqualifies a title:** vague modifiers ("ultimate", "complete"), the
|
||||
keyword stated twice, ChatGPT-default formatting ("A Comprehensive Guide to…"),
|
||||
or a hook so clickbait it overpromises what the body delivers.
|
||||
|
||||
---
|
||||
|
||||
## Images
|
||||
|
||||
Images aren't decoration — they're a quality signal Google uses (helpful
|
||||
content with original imagery outranks text-only posts in most categories), an
|
||||
engagement lever (reduces bounce, lifts dwell time), and an Open Graph asset
|
||||
that determines whether the post gets clicked on social.
|
||||
|
||||
### Minimum bar for a blog post
|
||||
- **1 featured/thumbnail image** — above the fold, also used as the Open Graph
|
||||
image and the post's `og:image` / `twitter:image` / structured-data `image`
|
||||
- **≥ 3 inline images** placed in the body at meaningful points — one per major
|
||||
H2 is a good rhythm
|
||||
|
||||
### What counts as a real image
|
||||
- **Diagrams** explaining a workflow, system, or relationship
|
||||
- **Screenshots** of the tool / dashboard / SERP being discussed
|
||||
- **Comparison visuals** (before/after, options side-by-side)
|
||||
- **Data visualization** of a stat the post cites
|
||||
- **Original illustration** that depicts something specific to the post
|
||||
|
||||
### What does NOT count
|
||||
- Stock photos of "person at laptop", handshakes, abstract gradients
|
||||
- Generic AI-generated "marketing hero" art with no informational content
|
||||
- The same image used decoratively in every section
|
||||
|
||||
### Image generation
|
||||
The agent should generate the images during the writing pass — don't hand the
|
||||
user a TODO list. Pick the first available surface:
|
||||
|
||||
1. **Host-native image gen** (Codex `gpt-image`, Gemini Imagen, etc.) — use it
|
||||
directly. Save to `images/` or the path the user picks.
|
||||
2. **NotFair MCP** — if a NotFair MCP is connected, `generate_image` produces
|
||||
marketing-grade visuals from a prompt; works for blog imagery too.
|
||||
3. **No image-gen available** — emit the detailed prompt for each image along
|
||||
with the markdown image tag (with the planned filename) so the post is ready
|
||||
to slot the asset in.
|
||||
|
||||
### Writing an image prompt
|
||||
Every prompt names: subject, style (photographic / 3D render / flat illustration
|
||||
/ data viz / diagram), composition, color palette, mood, and an explicit avoid
|
||||
list. Example:
|
||||
|
||||
> Featured image for a blog post on Facebook SEO optimization. Flat vector
|
||||
> illustration, isometric perspective. A stylized Facebook page card with search
|
||||
> bars, magnifying glass, and ranking arrows around it. Color palette: NotFair
|
||||
> orange (#FF6B35) accents on a clean off-white background, navy line work. No
|
||||
> stock-photo people, no generic laptop, no abstract gradients. Square 1200×1200.
|
||||
|
||||
### Image SEO
|
||||
- File name: keyword-relevant, hyphen-separated. `facebook-seo-page-checklist.png`
|
||||
- Alt text: descriptive of what the image shows, not keyword-stuffed
|
||||
- Format: WebP or AVIF preferred; PNG for diagrams with hard edges
|
||||
- Width: 1200px (featured), 800–1000px (inline), 2x for retina
|
||||
- Lazy-load every image except the featured one
|
||||
|
||||
### Media and Asset Quality
|
||||
- Use real, relevant visual assets for published pages when the topic benefits
|
||||
from illustration, inspection, or trust. Avoid purely decorative stock-like
|
||||
images that do not help the reader understand the product, place, process, or
|
||||
outcome.
|
||||
- Validate generated image files after export. The file extension, MIME/file
|
||||
signature, dimensions, and metadata declarations must agree.
|
||||
- Keep hero images reasonably sized for web delivery. Prefer WebP/AVIF photos at
|
||||
the displayed aspect ratio; oversized multi-megabyte source images are a
|
||||
technical UX issue, not just an asset issue.
|
||||
- Open Graph image dimensions in metadata must match the actual asset.
|
||||
|
||||
---
|
||||
|
||||
## Landing Pages
|
||||
|
||||
### When to Use
|
||||
Transactional and commercial intent: "buy", "pricing", "near me", "[service] in
|
||||
[city]", "best [product] for [use case]".
|
||||
|
||||
### Core Principles
|
||||
1. **Specificity converts** — replace every vague claim with numbers or examples
|
||||
2. **Benefits over features** — lead with what the customer gets, not what it does
|
||||
3. **Address objections directly** — price, trust, switching costs, "does it work?"
|
||||
4. **One page, one job** — every element moves toward the CTA
|
||||
5. **Social proof is mandatory** — testimonials, logos, stats, case studies
|
||||
|
||||
### Structure by Page Type
|
||||
|
||||
**Service page:**
|
||||
```
|
||||
H1: Service + Primary Benefit
|
||||
Hero subhead + Primary CTA
|
||||
H2: The Problem (show you understand their situation)
|
||||
H2: How It Works (3-4 clear steps)
|
||||
H2: Results (specific outcomes with numbers)
|
||||
H2: Social Proof (testimonials, logos, case studies)
|
||||
H2: Pricing (if applicable)
|
||||
H2: FAQ (5-8 objection-handling questions)
|
||||
Final CTA
|
||||
```
|
||||
|
||||
**Product page:**
|
||||
```
|
||||
H1: Product + Key Benefit
|
||||
Hero + CTA
|
||||
H2: Features → Benefits (not just feature lists)
|
||||
H2: How It Works
|
||||
H2: Who It's For (specific use cases)
|
||||
H2: Reviews / Social Proof
|
||||
H2: Pricing
|
||||
H2: FAQ
|
||||
Final CTA
|
||||
```
|
||||
|
||||
**Location page:**
|
||||
```
|
||||
H1: [Service] in [City]
|
||||
Local value prop + CTA
|
||||
H2: Local-specific content (NOT generic copy with city swapped)
|
||||
H2: Why Choose Us in [City] (local presence, case studies)
|
||||
H2: Service Areas
|
||||
H2: FAQ (local-specific concerns)
|
||||
CTA with local contact
|
||||
```
|
||||
|
||||
### SEO for Landing Pages
|
||||
- Lighter keyword density than blog posts
|
||||
- Primary keyword in: title, H1, first paragraph, 1 H2, meta description
|
||||
- 500-1500 words of genuine content (thin pages with just a headline + form don't rank)
|
||||
- FAQ section serves dual purpose: conversion (objection handling) + SEO (long-tail queries)
|
||||
|
||||
### Metadata
|
||||
- **Title tag:** "[Service/Product] — [Benefit] | [Brand]" (< 60 chars)
|
||||
- **Meta description:** 120-160 chars, includes CTA ("Get started free", "See pricing")
|
||||
- **URL slug:** `/[service-name]`, `/products/[product-name]`, `/[service]-[city]`
|
||||
- **Structured data:** Service page → `Service` + `FAQPage`; Product → `Product` + `Offer` + `FAQPage`; Local → `LocalBusiness` + `FAQPage`
|
||||
|
||||
---
|
||||
|
||||
## Search Intent Matching
|
||||
|
||||
| Intent | Keyword Signals | Content Type |
|
||||
|--------|----------------|-------------|
|
||||
| **Informational** | "how to", "what is", "guide" | Blog post, tutorial |
|
||||
| **Commercial** | "best", "vs", "review", "top" | Comparison, listicle |
|
||||
| **Transactional** | "buy", "price", "near me" | Landing/product page |
|
||||
| **Navigational** | brand name, product name | Homepage, product page |
|
||||
|
||||
**How to verify:** search the keyword incognito, look at top 5 results. Match
|
||||
that format — don't fight the SERP.
|
||||
|
||||
**Mismatch = won't rank:**
|
||||
- Blog post targeting "buy [product]" → should be product page
|
||||
- Product page targeting "how to [task]" → should be tutorial
|
||||
- Generic page targeting "[service] in [city]" → should be location page
|
||||
|
||||
---
|
||||
|
||||
## On-Page SEO Checklist
|
||||
|
||||
### Must-Have
|
||||
- [ ] Title tag with primary keyword, < 60 chars
|
||||
- [ ] Meta description with keyword + CTA, 120-160 chars
|
||||
- [ ] Single H1 with primary keyword
|
||||
- [ ] Logical heading hierarchy (H1 → H2 → H3)
|
||||
- [ ] Primary keyword in first 100 words naturally
|
||||
- [ ] Internal links to 3-5 related pages
|
||||
- [ ] Images have descriptive alt text
|
||||
- [ ] Image file signatures, dimensions, and metadata match the published assets
|
||||
- [ ] URL is short, readable, includes keyword slug
|
||||
- [ ] Compliance-sensitive claims are backed by official sources and scoped correctly
|
||||
|
||||
### Should-Have
|
||||
- [ ] Structured data (Article, Product, Service, HowTo, FAQPage)
|
||||
- [ ] External links to authoritative sources
|
||||
- [ ] Author byline with credentials
|
||||
- [ ] Last-updated date for evergreen content
|
||||
- [ ] Open Graph + Twitter Card meta tags
|
||||
|
||||
### Required for blog posts (in addition to Must-Have)
|
||||
- [ ] Hook-driven title — number, contrarian claim, named audience, or curiosity gap
|
||||
- [ ] Table of contents at the top with working anchor links
|
||||
- [ ] Featured/thumbnail image (also used as `og:image` / structured-data `image`)
|
||||
- [ ] ≥ 3 inline images (diagram, screenshot, comparison, or data viz)
|
||||
- [ ] ≥ 1000 words of substantive body content
|
||||
- [ ] FAQ section with 3–5 questions targeting People Also Ask
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
### Content Anti-Patterns
|
||||
- **Keyword stuffing** — use the keyword naturally; synonyms after the first few placements
|
||||
- **Thin content** — pages under 300 words competing for hard terms
|
||||
- **Content for content's sake** — topics with no real expertise behind them
|
||||
- **Wall of text** — no headings, no lists, no visual breaks
|
||||
- **Duplicate intent** — two pages targeting same keyword = cannibalization
|
||||
- **Stale content** — outdated stats, deprecated methods, old pricing
|
||||
|
||||
### AI Content Anti-Patterns
|
||||
Google doesn't penalize AI content per se — it penalizes unhelpful content.
|
||||
Common AI failure modes:
|
||||
- Generic summaries restating common knowledge
|
||||
- Excessive hedging ("it depends", "many factors") without commitments
|
||||
- Missing experience signals — no examples, data, or first-hand knowledge
|
||||
- Perfect grammar, zero original insight
|
||||
|
||||
**Fix:** inject real data, specific examples, original analysis, concrete
|
||||
recommendations. Make it something only someone with actual expertise could write.
|
||||
314
skills/geo-optimizer/SKILL.md
Normal file
314
skills/geo-optimizer/SKILL.md
Normal file
@@ -0,0 +1,314 @@
|
||||
---
|
||||
name: geo-optimizer
|
||||
argument-hint: "<URL, file path, or topic to optimize for AI search>"
|
||||
description: >
|
||||
Generative Engine Optimization (GEO) — make content rank in AI search
|
||||
answers from ChatGPT, Claude, Perplexity, Gemini, and Google AI Overviews.
|
||||
Audits existing content, rewrites for AI citation, and produces per-engine
|
||||
strategy. Use when asked to "optimize for AI search", "rank in ChatGPT",
|
||||
"GEO audit", "improve AI citations", "rank in Perplexity", "AI Overview
|
||||
optimization", "AI Overview ranking", "LLM SEO", "answer engine
|
||||
optimization", "AEO", "get cited by AI", "GEO", "generative engine
|
||||
optimization", "show up in ChatGPT", "appear in AI answers", "be cited
|
||||
by Perplexity", "SGE optimization", "Search Generative Experience", or
|
||||
"make my content show up in AI answers". Distinct from regular SEO —
|
||||
this targets generative engines, not traditional Google rankings.
|
||||
---
|
||||
|
||||
# GEO Optimizer
|
||||
|
||||
You are a Generative Engine Optimization specialist. Your job is to make
|
||||
content get cited, quoted, and referenced by AI search engines (ChatGPT,
|
||||
Claude, Perplexity, Gemini, Google AI Overviews) — not just rank in Google's
|
||||
blue links.
|
||||
|
||||
GEO is **not** SEO. The signals are different, the engines weigh evidence
|
||||
differently, and the wrong moves (keyword stuffing) actively hurt. This
|
||||
skill applies techniques validated by Princeton/GA Tech (KDD 2024) and
|
||||
CMU AutoGEO (ICLR 2026) research, adapted for production use.
|
||||
|
||||
You handle three jobs:
|
||||
1. **GEO audit** — score existing content against the GEO signal stack
|
||||
2. **GEO optimize** — rewrite content to maximize AI citation probability
|
||||
3. **GEO strategy** — produce an engine-specific playbook for a site
|
||||
|
||||
---
|
||||
|
||||
## Critical: No Fabrication. Ever.
|
||||
|
||||
The Princeton GEO paper showed fabricated quotes and citations boosted
|
||||
visibility against GPT-3.5 in 2023. **Do not replicate this.** Reasons:
|
||||
|
||||
- Engines now train on it as adversarial signal (StealthRank, 2025)
|
||||
- It exposes the user to FTC §5 violations and YMYL liability
|
||||
- One Reddit fact-check destroys their brand
|
||||
- C-SEO Bench (NeurIPS 2025) shows the lift evaporates under competition
|
||||
|
||||
**Find real evidence and apply it with the same structural patterns** that
|
||||
move PAWC (Position-Adjusted Word Count). You get 80–90% of the lift,
|
||||
zero of the legal risk, and content that survives scrutiny.
|
||||
|
||||
If the user explicitly asks you to fabricate stats or quotes, refuse and
|
||||
explain. This is non-negotiable.
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Determine the Job
|
||||
|
||||
Infer from the user's message:
|
||||
|
||||
- "audit", "score", "how is my page doing for AI", "is this GEO-ready" → **Audit**
|
||||
- "optimize", "rewrite", "improve for AI search", "make this rank in ChatGPT" → **Optimize**
|
||||
- "strategy for [site]", "GEO playbook", "where should I focus" → **Strategy**
|
||||
|
||||
If ambiguous, ask once: "Audit (score this page), Optimize (rewrite for
|
||||
AI citation), or Strategy (full playbook for the site)?"
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Read the Reference
|
||||
|
||||
Before any work, locate and read the GEO techniques reference:
|
||||
|
||||
```bash
|
||||
GEO_REF=$(find ~/.claude/plugins ~/.claude/skills ~/.codex/skills .agents/skills -name "geo-techniques.md" -path "*geo-optimizer*" 2>/dev/null | head -1)
|
||||
if [ -z "$GEO_REF" ]; then
|
||||
GEO_REF="references/geo-techniques.md"
|
||||
fi
|
||||
```
|
||||
|
||||
Read `$GEO_REF`. The signal weights, density targets, audit scoring,
|
||||
rewrite patterns, and per-engine playbooks all live there. Follow it
|
||||
precisely throughout Steps 3–6.
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Gather Context
|
||||
|
||||
### For Audit or Optimize:
|
||||
- **The content** — fetch URL via WebFetch, read file path, or ask for paste
|
||||
- **Target query/topic** — what AI question should this content answer?
|
||||
- **Target engines** — ChatGPT, Perplexity, Claude, Gemini, AI Overviews
|
||||
(default: all four; the playbooks differ)
|
||||
- **Brand/site context** — what does the org do, who's the author?
|
||||
|
||||
### For Strategy:
|
||||
- **The site** — domain
|
||||
- **Current state** — do they have GSC data, brand searches, citations now?
|
||||
- **Goal** — defensive (already cited, want to keep it) or offensive
|
||||
(not cited, want to break in)
|
||||
|
||||
Don't ask for things you can infer. If the user pasted a URL, just fetch it.
|
||||
|
||||
---
|
||||
|
||||
## Step 4 — Execute
|
||||
|
||||
### Mode A: Audit
|
||||
|
||||
Score the content against the **GEO Signal Stack** in `geo-techniques.md`.
|
||||
Output a **GEO Score (0–100)** broken into four pillars:
|
||||
|
||||
1. **Evidence Density (35%)** — quotations, statistics, citations, named entities
|
||||
2. **Structure & Position (25%)** — front-loading, scannability, schema
|
||||
3. **Authority Signals (25%)** — author identity, originality, freshness
|
||||
4. **AI Crawlability (15%)** — SSR, robots.txt, schema, llms.txt
|
||||
|
||||
For each item, return: ✅ pass / ⚠️ partial / ❌ fail + **what to fix**.
|
||||
|
||||
Apply **veto checks** (auto-cap score at 60):
|
||||
- Self-contradictory data on the page
|
||||
- Title-content intent mismatch (clickbait)
|
||||
- Missing author / no first-party identity
|
||||
- Blocked AI crawlers (GPTBot, ClaudeBot, PerplexityBot, Google-Extended)
|
||||
- YMYL content (health, finance, legal, safety) without appropriate
|
||||
disclaimers or qualified-author byline
|
||||
- Fabricated citations, statistics, or expert names detected — this is
|
||||
a hard fail, not a cap. Refuse to produce the audit and explain.
|
||||
|
||||
Output format:
|
||||
|
||||
```
|
||||
# GEO Audit: [URL or title]
|
||||
|
||||
## GEO Score: [N]/100
|
||||
|
||||
### Pillar Breakdown
|
||||
- Evidence Density: [N]/35
|
||||
- Structure & Position: [N]/25
|
||||
- Authority Signals: [N]/25
|
||||
- AI Crawlability: [N]/15
|
||||
|
||||
### Top 5 Fixes (Highest Lift First)
|
||||
1. [Fix] — Expected lift: [N points] — Effort: [low/med/high]
|
||||
[Specific, actionable change with location in content]
|
||||
...
|
||||
|
||||
### Detailed Findings
|
||||
[Item-by-item pass/partial/fail with explanation]
|
||||
|
||||
### Vetoes Triggered
|
||||
[Any. Or "None."]
|
||||
|
||||
### Recommended Next Step
|
||||
- "Run /geo-optimizer optimize on this page" to apply the fixes, OR
|
||||
- [Strategic guidance if structural issues block on-page work]
|
||||
```
|
||||
|
||||
### Mode B: Optimize
|
||||
|
||||
Rewrite the content applying the techniques in priority order:
|
||||
|
||||
**Priority 1 — Front-load the answer.**
|
||||
The first 150 words must directly answer the target query. PAWC's exponential
|
||||
decay means sentence #1 is worth ~5× sentence #20.
|
||||
|
||||
**Priority 2 — Real evidence at density.**
|
||||
Targets (per `geo-techniques.md`):
|
||||
- ≥5 specific numbers with units (%, $, ms, days, kg, etc.)
|
||||
- ≥1 external citation per 500 words, ≥3 source types
|
||||
- ≥2 direct quotes from named experts (real ones — search for them)
|
||||
- ≥3 named entities (people, orgs, products) with full names
|
||||
|
||||
**The Evidence Hunt is mandatory before rewriting.** If you have web access
|
||||
(WebSearch, WebFetch, browse), find real sources. If not, ask the user for
|
||||
their internal data or pause and request sources. Never invent.
|
||||
|
||||
**Priority 3 — Structure for extraction.**
|
||||
- TL;DR or Key Takeaways box near top
|
||||
- Comparison data → HTML tables
|
||||
- Sequential steps → numbered lists
|
||||
- Definitions → defined on first use, ideally in a definition block
|
||||
- FAQ section with `FAQPage` schema
|
||||
|
||||
**Priority 4 — Add JSON-LD.**
|
||||
`Article`/`BlogPosting` + `FAQPage` minimum. `HowTo` for procedural content.
|
||||
`Product` for commercial. Author with `sameAs` to Wikipedia/LinkedIn/ORCID.
|
||||
|
||||
**Priority 5 — Strip GEO anti-patterns.**
|
||||
- Remove keyword stuffing (−8% PAWC)
|
||||
- Remove filler ("In today's digital landscape…")
|
||||
- Remove unsupported superlatives ("the best", "leading provider")
|
||||
- Remove vague entities ("a company", "experts say")
|
||||
|
||||
Output format:
|
||||
|
||||
```
|
||||
# GEO Optimization: [Title]
|
||||
|
||||
## Changes Applied
|
||||
- [Fluency rewrite, +X% expected]
|
||||
- [Statistics added: N stats from M sources]
|
||||
- [Citations added: N citations]
|
||||
- [Quotations added: N expert quotes]
|
||||
- [Front-loaded answer in first 150 words]
|
||||
- [Schema added: types]
|
||||
- [Removed: keyword stuffing in section X, filler in section Y]
|
||||
|
||||
## Sources Used (verify before publishing)
|
||||
1. [Real URL] — used for [stat/quote]
|
||||
2. ...
|
||||
|
||||
## Rewritten Content
|
||||
[Full markdown]
|
||||
|
||||
## SEO + GEO Metadata
|
||||
- Title tag: [< 60 chars]
|
||||
- Meta description: [120-160 chars]
|
||||
- URL slug: /[slug]
|
||||
- Target query: [primary]
|
||||
- Target engines: [list]
|
||||
|
||||
## Structured Data
|
||||
[JSON-LD]
|
||||
|
||||
## Pre-Publish Checklist
|
||||
- [ ] All sources verified (URLs work, quotes accurate)
|
||||
- [ ] Author byline + sameAs links present
|
||||
- [ ] Last-updated date set to today
|
||||
- [ ] AI crawlers allowed in robots.txt
|
||||
- [ ] FAQPage schema renders in https://search.google.com/test/rich-results
|
||||
- [ ] No fabricated stats/quotes (re-read once more)
|
||||
```
|
||||
|
||||
### Mode C: Strategy
|
||||
|
||||
Produce a 30/60/90 day GEO playbook for the site, structured by `geo-techniques.md`
|
||||
section "Per-Engine Playbooks". Required sections:
|
||||
|
||||
1. **Current state** — if you have web access, check: is the site cited
|
||||
in ChatGPT/Perplexity for its core queries? Run a few brand + category
|
||||
queries and note results.
|
||||
2. **30 days — On-site fixes** — pages to optimize, in ranked order by
|
||||
traffic potential × current GEO score gap
|
||||
3. **60 days — Authority building** — Wikipedia, Reddit, Stack Overflow,
|
||||
industry media, original-data publications
|
||||
4. **90 days — Engine-specific moves** — per ChatGPT, Perplexity, Claude,
|
||||
Gemini, AI Overviews
|
||||
5. **Measurement** — what to track and how (cite gego, llmopt patterns)
|
||||
|
||||
---
|
||||
|
||||
## Step 5 — Quality Gate
|
||||
|
||||
Before delivering, run these checks. Fix failures before presenting.
|
||||
|
||||
### Fabrication Check (mandatory)
|
||||
- Every stat has a real, verifiable source URL
|
||||
- Every quote attributed to a real, named person at a real org
|
||||
- No "according to a 2024 study" without the actual study citation
|
||||
- No invented expert names
|
||||
|
||||
If any fail → don't deliver. Find real evidence or flag the gap to the user.
|
||||
|
||||
### PAWC Front-Loading Check
|
||||
- Does the first sentence after the H1 directly answer the target query?
|
||||
- Could a reader who only saw the first 150 words walk away with the answer?
|
||||
|
||||
### Evidence Density Check
|
||||
- Count: numbers with units, citations, quotes, named entities
|
||||
- Compare against the targets in `geo-techniques.md`
|
||||
|
||||
### Anti-Pattern Check
|
||||
- No keyword stuffing (search for the target keyword — appears > 1% of word count?)
|
||||
- No vague entities or unsupported superlatives
|
||||
- No filler intros
|
||||
|
||||
### AI Crawlability Check (Optimize mode only)
|
||||
- robots.txt allows: GPTBot, ClaudeBot, PerplexityBot, Google-Extended,
|
||||
PerplexityBot, Bytespider, anthropic-ai, ChatGPT-User
|
||||
- Critical content is server-rendered (not behind JS-only)
|
||||
- Schema validates
|
||||
|
||||
### Schema Check
|
||||
- JSON-LD parses
|
||||
- Required fields present (`@context`, `@type`, `headline`, `author`,
|
||||
`datePublished`, `dateModified`)
|
||||
- `author.sameAs` includes verifiable identity links
|
||||
|
||||
---
|
||||
|
||||
## Step 6 — Hand Off
|
||||
|
||||
After delivering, suggest the natural next step:
|
||||
|
||||
- **Audit completed** → "Want me to optimize this page? Run me with `optimize`."
|
||||
- **Optimize completed** → "Want a strategy for the rest of the site? Run me with `strategy`."
|
||||
- **Strategy completed** → "Want me to start optimizing the highest-priority page from the list?"
|
||||
|
||||
If a CMS is configured and the user wants to push the rewritten content,
|
||||
use the `seo-analysis` CMS push flow (currently supports Strapi). For
|
||||
other CMSes, the user manually applies the markdown output.
|
||||
|
||||
---
|
||||
|
||||
## Coordination With Other Skills
|
||||
|
||||
- **`content-writer`** writes for Google's blue links (E-E-A-T, helpful content).
|
||||
This skill writes for AI engines (PAWC, evidence density). Use both for
|
||||
pages that need to win both surfaces.
|
||||
- **`seo-analysis`** identifies which pages to optimize. Use it first if
|
||||
the user hasn't picked a page.
|
||||
- **`schema-markup-generator`** can produce the JSON-LD if the rewrite
|
||||
needs complex schema (HowTo, multi-entity Article).
|
||||
- **`meta-tags-optimizer`** finalizes title + meta description after rewrite.
|
||||
18
skills/geo-optimizer/evals/evals.json
Normal file
18
skills/geo-optimizer/evals/evals.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"skill_name": "geo-optimizer",
|
||||
"evals": [
|
||||
{
|
||||
"id": 1,
|
||||
"prompt": "do a GEO audit of https://example.com/blog/best-crm-software and tell me how likely it is to get cited by ChatGPT, Claude, Perplexity, and Google AI Overviews",
|
||||
"expected_output": "A GEO audit that evaluates evidence density, structure, authority signals, and AI-citation readiness across major answer engines with concrete next steps.",
|
||||
"files": [],
|
||||
"expectations": [
|
||||
"Recognizes this as a GEO audit request",
|
||||
"Assesses AI-search readiness rather than traditional SEO alone",
|
||||
"Discusses evidence density, structure, or authority signals",
|
||||
"Differentiates considerations across multiple AI engines",
|
||||
"Provides concrete recommendations without fabricating evidence"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
592
skills/geo-optimizer/references/geo-techniques.md
Normal file
592
skills/geo-optimizer/references/geo-techniques.md
Normal file
@@ -0,0 +1,592 @@
|
||||
# GEO Techniques — Generative Engine Optimization Playbook
|
||||
|
||||
Reference for `geo-optimizer`. Derived from:
|
||||
|
||||
- **Princeton/GA Tech GEO** (KDD 2024, arXiv:2311.09735) — the 9 methods,
|
||||
PAWC metric, GPT-3.5 / Perplexity validation
|
||||
- **AutoGEO** (CMU, ICLR 2026) — automated rewriting, GRPO training,
|
||||
utility-preserving rewrite rules
|
||||
- **C-SEO Bench** (NeurIPS 2025) — competitive baseline, what survives at scale
|
||||
- **CORE-EEAT / CITE** (community frameworks) — operational checklists
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Core Principles](#core-principles)
|
||||
2. [The GEO Signal Stack](#the-geo-signal-stack)
|
||||
3. [Audit Scoring](#audit-scoring)
|
||||
4. [Rewrite Patterns](#rewrite-patterns)
|
||||
5. [Evidence Hunt — Finding Real Sources](#evidence-hunt--finding-real-sources)
|
||||
6. [Per-Engine Playbooks](#per-engine-playbooks)
|
||||
7. [AI Crawlability](#ai-crawlability)
|
||||
8. [Anti-Patterns](#anti-patterns)
|
||||
9. [Measurement](#measurement)
|
||||
|
||||
---
|
||||
|
||||
## Core Principles
|
||||
|
||||
### 1. PAWC drives everything
|
||||
|
||||
Position-Adjusted Word Count is the metric the Princeton paper proved
|
||||
correlates with AI citation:
|
||||
|
||||
```
|
||||
Imp_pwc(c, r) = Σ |sentence| · e^(-pos/total) / total_words
|
||||
```
|
||||
|
||||
The exponential decay is the key: **sentence #1 of the AI's answer is
|
||||
worth ~5× sentence #20.** If you want to be cited, your content must
|
||||
show up in the *first* part of the AI's answer, which means your
|
||||
*first* sentences must be the most extractable, evidence-dense ones.
|
||||
|
||||
### 2. Evidence density > keyword density
|
||||
|
||||
Princeton's empirical ranking of techniques by visibility lift:
|
||||
|
||||
| Rank | Technique | PAWC lift |
|
||||
|------|-----------|-----------|
|
||||
| 1 | Quotation Addition | +41% |
|
||||
| 2 | Statistics Addition | +30% |
|
||||
| 3 | Cite Sources | +28% |
|
||||
| 3 | Fluency Optimization | +28% |
|
||||
| 5 | Technical Terms | +18% |
|
||||
| 6 | Easy-to-Understand | +14% |
|
||||
| 7 | Authoritative tone | +10% |
|
||||
| 8 | Unique Words | +6% |
|
||||
| 9 | **Keyword Stuffing** | **−8%** (hurts) |
|
||||
|
||||
Best combo: **Fluency + Statistics** (≥+35%, beats any single technique).
|
||||
|
||||
### 3. Generative engines don't use PageRank
|
||||
|
||||
This is the democratization finding from the Princeton GEO paper
|
||||
(arXiv:2311.09735, Table 2): rank-5 sites gained ~+115% visibility with
|
||||
the Cite Sources method while rank-1 sites *lost* ~30%, averaged across
|
||||
their multi-domain experiment. Numbers are representative of the paper's
|
||||
test setup, not a universal guarantee. The implication still holds:
|
||||
weaker-authority sites can punch up dramatically by adding evidence
|
||||
signals, because the LLM doesn't apply PageRank-style domain weighting
|
||||
when citing. **It cares whether your sentence is the most quotable one.**
|
||||
|
||||
### 4. Engines diverge
|
||||
|
||||
Cross-engine citation overlap is 0.11–0.58 (Princeton + AutoGEO data).
|
||||
Optimize per-engine:
|
||||
|
||||
- **ChatGPT** cites Wikipedia in ~48% of top citations
|
||||
- **Perplexity** cites recent web sources, weights freshness
|
||||
- **Gemini** leans Reddit/Quora for opinion queries
|
||||
- **Claude** weights primary sources and academic citations
|
||||
- **Google AI Overviews** mirrors organic top-10 + featured snippets
|
||||
|
||||
### 5. Real evidence wins long-term
|
||||
|
||||
Princeton showed fabricated quotes worked against GPT-3.5. AutoGEO's
|
||||
real-engine training explicitly says "substantiate claims with concrete
|
||||
details." Engines have moved on. Build with real sources only.
|
||||
|
||||
---
|
||||
|
||||
## The GEO Signal Stack
|
||||
|
||||
Four pillars, weighted as in the audit scoring:
|
||||
|
||||
### Pillar 1 — Evidence Density (35%)
|
||||
|
||||
| Signal | Target | Why |
|
||||
|--------|--------|-----|
|
||||
| Numbers with units | ≥5 per article | LLMs preferentially extract specific numerics |
|
||||
| External citations | ≥1 per 500 words, ≥3 source types | Authority + verifiability |
|
||||
| Direct expert quotes | ≥2 from named individuals | Quotation Addition is the +41% method |
|
||||
| Named entities | ≥3 with full names + roles | Specificity beats vagueness |
|
||||
| First-party data | ≥1 original stat or framework | Becomes the only-citable source |
|
||||
|
||||
### Pillar 2 — Structure & Position (25%)
|
||||
|
||||
| Signal | Target |
|
||||
|--------|--------|
|
||||
| Direct answer in first 150 words | Required (PAWC) |
|
||||
| TL;DR or Key Takeaways near top | ≥1 box |
|
||||
| Heading hierarchy (H1→H2→H3) | No level skipping, single H1 |
|
||||
| Comparison/spec data in tables | Required if comparison content |
|
||||
| Sequential steps in numbered lists | Required if procedural |
|
||||
| FAQ section with question-format H2/H3 | Required for informational |
|
||||
| Average paragraph length | 2–4 sentences |
|
||||
| JSON-LD schema | `Article` minimum, `FAQPage` if FAQ, `HowTo` if procedural |
|
||||
|
||||
### Pillar 3 — Authority Signals (25%)
|
||||
|
||||
| Signal | Target |
|
||||
|--------|--------|
|
||||
| Author byline | Real name, role, ≥30-word bio |
|
||||
| `author.sameAs` JSON-LD | Wikipedia, LinkedIn, ORCID, Google Scholar |
|
||||
| Last updated within 60 days | Recency (3× citation lift per Princeton + amplifying-ai data); 60–90 days is the boundary, target 60 |
|
||||
| Methodology disclosed | Sample sizes, criteria, dates |
|
||||
| Limitations acknowledged | Counter-LLM-hallucination signal |
|
||||
| First-party experience markers | "We tested", "Our analysis of N…" — not vague "experts say" |
|
||||
| External validators | Featured in / cited by named outlets |
|
||||
|
||||
### Pillar 4 — AI Crawlability (15%)
|
||||
|
||||
| Signal | Target |
|
||||
|--------|--------|
|
||||
| robots.txt allows AI bots | GPTBot, ClaudeBot, PerplexityBot, Google-Extended, anthropic-ai, ChatGPT-User, Bytespider |
|
||||
| Server-side rendered content | Critical content not JS-only |
|
||||
| `llms.txt` at site root | Optional but adopted by 784+ sites as of mid-2025 |
|
||||
| HTTPS + HSTS | Required |
|
||||
| Canonical URLs | Required |
|
||||
| `<time>` tags + `dateModified` | Required for freshness signal |
|
||||
| Schema validates | Use Rich Results Test |
|
||||
|
||||
---
|
||||
|
||||
## Audit Scoring
|
||||
|
||||
For each item in the signal stack, score:
|
||||
|
||||
- **Pass (full points)** — meets target
|
||||
- **Partial (50%)** — partial implementation
|
||||
- **Fail (0)** — missing or wrong direction (e.g., keyword stuffing present)
|
||||
|
||||
Sum to a 0–100 GEO Score with the pillar weights.
|
||||
|
||||
### Veto items (auto-cap at 60)
|
||||
|
||||
These either kill citation or expose the user to liability:
|
||||
|
||||
1. **Self-contradictory data** — internal inconsistency on the page
|
||||
2. **Title-content intent mismatch** — clickbait
|
||||
3. **No identifiable author** — anonymous content rarely gets cited as primary source
|
||||
4. **AI crawlers blocked** in robots.txt or CDN/WAF
|
||||
5. **Fabricated citations or stats** detected — hard fail, not a cap
|
||||
6. **YMYL content without disclaimers** — health/finance/legal without appropriate warnings
|
||||
|
||||
### GEO Score interpretation
|
||||
|
||||
- **80–100** — well-positioned for AI citation; iterate on per-engine playbooks
|
||||
- **60–79** — solid foundation, missing 1–3 high-leverage signals
|
||||
- **40–59** — structural fixes needed before per-engine work pays off
|
||||
- **0–39** — rewrite from outline; current content unlikely to be cited
|
||||
|
||||
---
|
||||
|
||||
## Rewrite Patterns
|
||||
|
||||
Apply in this priority order. Stop when the content is at quality bar; not
|
||||
every page needs every pattern.
|
||||
|
||||
### Pattern 1 — Front-Load the Answer
|
||||
|
||||
**Before:**
|
||||
> "In today's rapidly evolving digital landscape, businesses are constantly
|
||||
> seeking ways to optimize their online presence. This article will explore
|
||||
> the various strategies and considerations involved in [topic]."
|
||||
|
||||
**After (template — replace bracketed values with real, verified data
|
||||
before publishing):**
|
||||
> "[Topic]'s ROI averages [REAL_NUMBER]× ([REAL_SOURCE_WITH_URL], [YEAR])
|
||||
> when implemented with [specific approach]. Three steps drive that lift:
|
||||
> [step 1], [step 2], [step 3]. Below: how to implement each in 2 weeks."
|
||||
|
||||
The first sentence carries: a specific number, a unit, a real-source
|
||||
citation, a year, and a concrete preview. PAWC will weigh this sentence
|
||||
~5× any conclusion paragraph. **Do not ship the template values** — every
|
||||
bracketed value must be replaced with a real, verifiable fact before this
|
||||
content goes live. Run the Evidence Hunt section below to source them.
|
||||
|
||||
### Pattern 2 — Statistics Addition (real)
|
||||
|
||||
**Before:**
|
||||
> "Many companies struggle with onboarding."
|
||||
|
||||
**After (template):**
|
||||
> "[REAL_PERCENT]% of [defined population] report [specific finding]
|
||||
> ([REAL_SOURCE_NAME_WITH_URL], [year], n=[real sample size])."
|
||||
|
||||
Rule: every claim that can be quantified, must be. Hunt for the real stat
|
||||
before falling back to vague language. If the stat doesn't exist publicly,
|
||||
follow the "What to do when the stat doesn't exist" section below — never
|
||||
keep the claim as a vague unsourced statement.
|
||||
|
||||
### Pattern 3 — Quotation Addition (real)
|
||||
|
||||
**Before:**
|
||||
> "Experts agree that retention is more cost-effective than acquisition."
|
||||
|
||||
**After (template):**
|
||||
> "'[Verbatim quote from a real, named person],' [wrote/said]
|
||||
> [Real Name] in *[Real Publication Title]* ([Publisher], [Year]),
|
||||
> [one-line context establishing why this person is authoritative]."
|
||||
|
||||
A real working example for the retention claim above: Frederick Reichheld
|
||||
& Earl Sasser's "Zero Defections: Quality Comes to Services" (Harvard
|
||||
Business Review, Sep–Oct 1990) is the canonical retention-economics
|
||||
citation. Verify the quote and URL before publishing.
|
||||
|
||||
Rule: cite a real person at a real org with a real publication. If you
|
||||
can't find one for the claim, the claim probably isn't load-bearing.
|
||||
|
||||
### Pattern 4 — Citation Addition (real)
|
||||
|
||||
**Before:**
|
||||
> "Search behavior has shifted toward AI assistants."
|
||||
|
||||
**After (template):**
|
||||
> "[Specific stat]% of [defined activity] now [specific behavior]
|
||||
> ([Real Research Firm], [Month Year], [URL]) versus [historical stat]
|
||||
> in [comparison year], with [observed pattern]."
|
||||
|
||||
Rule: ≥1 citation per 500 words, ≥3 source types per article. Source types
|
||||
include: peer-reviewed papers, government data, industry research firms,
|
||||
named publications, primary first-party data. Every citation must include
|
||||
a URL the reader can click — citations without verifiable URLs do not
|
||||
count toward the density target and trigger the fabrication veto.
|
||||
|
||||
### Pattern 5 — Fluency Optimization
|
||||
|
||||
The +28% lift from this method requires no new facts. It's just rewriting
|
||||
for flow. Apply it last, after you've added evidence.
|
||||
|
||||
Rules:
|
||||
- One idea per paragraph
|
||||
- Sentence variety: alternate short/medium/long
|
||||
- Active voice by default
|
||||
- Cut every word that doesn't earn its place
|
||||
- Read aloud test — if you stumble, rewrite
|
||||
|
||||
### Pattern 6 — Schema Markup
|
||||
|
||||
Minimum for any article-style content:
|
||||
|
||||
```json
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
"headline": "[H1]",
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": "[Real name]",
|
||||
"url": "[Author page URL]",
|
||||
"sameAs": [
|
||||
"https://en.wikipedia.org/wiki/[Author]",
|
||||
"https://www.linkedin.com/in/[handle]",
|
||||
"https://orcid.org/[id]"
|
||||
]
|
||||
},
|
||||
"datePublished": "[ISO date]",
|
||||
"dateModified": "[ISO date]",
|
||||
"publisher": {
|
||||
"@type": "Organization",
|
||||
"name": "[Org]",
|
||||
"logo": {"@type": "ImageObject", "url": "[Logo URL]"}
|
||||
},
|
||||
"mainEntityOfPage": "[Canonical URL]"
|
||||
}
|
||||
```
|
||||
|
||||
Add `FAQPage` if FAQ section present. Add `HowTo` if procedural. Validate
|
||||
at `search.google.com/test/rich-results` before publishing.
|
||||
|
||||
---
|
||||
|
||||
## Evidence Hunt — Finding Real Sources
|
||||
|
||||
Before any rewrite, build a source list. Tools in priority order:
|
||||
|
||||
1. **WebSearch** — for recent stats and named studies
|
||||
2. **WebFetch on primary source pages** — verify the stat exists at the URL
|
||||
3. **Google Scholar** (`scholar.google.com/scholar?q=...`) — academic
|
||||
4. **Government data portals** — `data.gov`, `bls.gov`, `eurostat.ec.europa.eu`,
|
||||
`data.gov.uk`
|
||||
5. **Named research firms** — Pew, Forrester, McKinsey, Gartner, Statista
|
||||
(cite the firm + publication date + report name)
|
||||
6. **Primary publications** — NYT, FT, WSJ, The Economist, trade press
|
||||
relevant to the topic
|
||||
7. **First-party data from the user** — ask: "Do you have any internal data
|
||||
that supports this claim?" Original first-party data is the strongest
|
||||
GEO signal.
|
||||
|
||||
### Verification rules
|
||||
|
||||
- **Every stat must trace to a URL you've actually fetched**
|
||||
- **Every quote must come from a real publication you can cite by name**
|
||||
- **Every named expert must be a real person at a real org**
|
||||
- If you can't verify, reframe the claim or remove it
|
||||
|
||||
### What to do when the stat doesn't exist
|
||||
|
||||
If you genuinely can't find a real source for a claim, in order of preference:
|
||||
|
||||
1. **Anchor to a related, verifiable stat** — "the broader [parent category]
|
||||
grew 12% in 2024 (Source, URL)" with a real source for the parent number.
|
||||
This is acceptable because the citation is real and the relationship is
|
||||
stated honestly.
|
||||
2. **Run an internal analysis** — if the user has data, use it. First-party
|
||||
data is the strongest GEO signal anyway.
|
||||
3. **Drop the claim** — if it's not load-bearing, cut it.
|
||||
|
||||
**Do not** keep the claim as a vague directional statement ("growing
|
||||
rapidly", "increasingly common", "many companies"). That violates the
|
||||
vague-entity anti-pattern below — vague unsourced statements are still
|
||||
fabrication-adjacent and dilute the page's evidence density.
|
||||
|
||||
Never invent. Not "according to a 2024 study", not "experts estimate", not
|
||||
"surveys show". Real source with URL, or no claim.
|
||||
|
||||
---
|
||||
|
||||
## Per-Engine Playbooks
|
||||
|
||||
Cross-engine citation overlap is 0.11–0.58. Tailor the strategy.
|
||||
|
||||
### ChatGPT (OpenAI)
|
||||
|
||||
**Citation pattern:** Wikipedia ~48% of top citations; reputable publications;
|
||||
moderate freshness preference.
|
||||
|
||||
**Optimization moves:**
|
||||
- Build/maintain a Wikipedia presence for the entity (brand, person, product)
|
||||
- Get listed in Wikidata with structured properties
|
||||
- Earn coverage in citations Wikipedia accepts (NYT, FT, BBC, Reuters,
|
||||
industry trade press)
|
||||
- Strong author-as-entity signaling (`sameAs` to Wikipedia)
|
||||
- Comprehensive reference articles outrank thin "answer" pages
|
||||
|
||||
### Perplexity
|
||||
|
||||
**Citation pattern:** Heavy on recent web; cites primary sources directly;
|
||||
fewer "synthesis" citations.
|
||||
|
||||
**Optimization moves:**
|
||||
- Recency matters most — pages updated within 90 days outperform
|
||||
- Original first-party data gets cited disproportionately
|
||||
- Clear thesis sentences in the first paragraph
|
||||
- Industry blog content with named author + date stamps
|
||||
- Tracking: Perplexity Sonar API exposes which URLs were cited
|
||||
(gego repo automates this); use it to verify
|
||||
|
||||
### Gemini (Google)
|
||||
|
||||
**Citation pattern:** Reddit / Quora prominent for opinion / advice queries;
|
||||
Google search index parity.
|
||||
|
||||
**Optimization moves:**
|
||||
- Reddit presence: maintain authoritative subreddit comments under named
|
||||
account; AMA-style threads
|
||||
- Quora answers from credentialed account
|
||||
- Strong on-page Google SEO — Gemini citations correlate with organic
|
||||
top-10
|
||||
- Google AI Overviews specifically: structured data + featured-snippet
|
||||
format wins
|
||||
|
||||
### Claude (Anthropic)
|
||||
|
||||
**Citation pattern:** Primary sources, academic citations, well-structured
|
||||
explanatory content.
|
||||
|
||||
**Optimization moves:**
|
||||
- Long-form, well-cited articles outperform short-form
|
||||
- Named author with verifiable credentials in `author.sameAs`
|
||||
- Citations to peer-reviewed sources where applicable
|
||||
- Limitations and methodology disclosed (counter-hallucination signaling)
|
||||
- Avoid marketing language — Claude weights informational tone heavily
|
||||
|
||||
### Google AI Overviews
|
||||
|
||||
**Citation pattern:** ~85% overlap with organic top 10 + featured snippets.
|
||||
|
||||
**Optimization moves:**
|
||||
- Win the featured snippet for the query (definition box, list, table)
|
||||
- Schema markup (`Article`, `FAQPage`, `HowTo`)
|
||||
- Direct answer in 40–60 words near top of page
|
||||
- Page must already rank top 10 organically — GEO doesn't bypass SEO here
|
||||
|
||||
### Cross-engine moves (do these first)
|
||||
|
||||
- llms.txt at site root with content map
|
||||
- Author entities with strong `sameAs` linkage
|
||||
- Original data publications quarterly
|
||||
- Wikipedia / Wikidata presence for the brand
|
||||
- Reddit + Stack Overflow + relevant community presence
|
||||
|
||||
---
|
||||
|
||||
## AI Crawlability
|
||||
|
||||
### robots.txt — must allow
|
||||
|
||||
```
|
||||
User-agent: GPTBot
|
||||
Allow: /
|
||||
|
||||
User-agent: ChatGPT-User
|
||||
Allow: /
|
||||
|
||||
User-agent: ClaudeBot
|
||||
Allow: /
|
||||
|
||||
User-agent: anthropic-ai
|
||||
Allow: /
|
||||
|
||||
User-agent: PerplexityBot
|
||||
Allow: /
|
||||
|
||||
User-agent: Perplexity-User
|
||||
Allow: /
|
||||
|
||||
User-agent: Google-Extended
|
||||
Allow: /
|
||||
|
||||
User-agent: Bytespider
|
||||
Allow: /
|
||||
|
||||
User-agent: Applebot-Extended
|
||||
Allow: /
|
||||
|
||||
User-agent: cohere-ai
|
||||
Allow: /
|
||||
|
||||
User-agent: meta-externalagent
|
||||
Allow: /
|
||||
```
|
||||
|
||||
If the user is currently blocking these (often inherited from default
|
||||
"block all bots" templates), this is the single highest-leverage fix.
|
||||
|
||||
### Optional: llms.txt
|
||||
|
||||
Adopted by 784+ sites as of mid-2025; not yet a confirmed ranking signal
|
||||
but trending. Place at site root:
|
||||
|
||||
```
|
||||
# Site Name
|
||||
|
||||
> One-paragraph description of the site, what it does, who it's for.
|
||||
|
||||
## Core content
|
||||
|
||||
- [Page Title](URL): One-line summary
|
||||
- [Page Title](URL): One-line summary
|
||||
|
||||
## About
|
||||
|
||||
- [About](URL)
|
||||
- [Contact](URL)
|
||||
|
||||
## Optional
|
||||
|
||||
- [Old content](URL): Archive
|
||||
```
|
||||
|
||||
### CDN / WAF
|
||||
|
||||
Cloudflare, AWS WAF, and Akamai often block AI bots by default. Verify
|
||||
in the CDN dashboard separately from robots.txt — robots.txt being
|
||||
permissive doesn't help if the WAF returns 403.
|
||||
|
||||
### Server-side rendering
|
||||
|
||||
JS-only content (CSR-heavy SPAs without prerendering) is invisible to
|
||||
most AI crawlers. Use Next.js / Nuxt / Astro / Remix server rendering
|
||||
or static generation for any page that should be cited.
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
These either don't work or actively hurt:
|
||||
|
||||
### Hard fails (will cause penalties or removal)
|
||||
|
||||
- **Fabricated citations / quotes / stats** — see Step 5 of SKILL.md
|
||||
- **Hidden text optimization** — old SEO trick; AI engines detect and demote
|
||||
- **Doorway pages** — single-purpose pages targeting near-duplicate queries
|
||||
- **AI-generated mass content with no human review** — both Google and
|
||||
AI engines now penalize
|
||||
- **PBN backlink networks** — CITE framework veto item
|
||||
|
||||
### Soft fails (waste of effort)
|
||||
|
||||
- **Keyword stuffing** — Princeton: −8% PAWC. Stop.
|
||||
- **Generic AI-language intros** — "In today's rapidly evolving landscape…"
|
||||
Cut.
|
||||
- **Vague entities** — "a leading company", "experts say", "studies show".
|
||||
Specify or remove.
|
||||
- **Unsupported superlatives** — "the best", "the most comprehensive".
|
||||
Either back with data or cut.
|
||||
- **Filler paragraphs** — every paragraph must earn its place
|
||||
- **Redundant H2s covering the same subtopic** — cannibalizes extraction
|
||||
|
||||
---
|
||||
|
||||
## Measurement
|
||||
|
||||
GEO without measurement is a vibe. Set up at minimum:
|
||||
|
||||
### Citation tracking
|
||||
|
||||
- **gego** — open source (Go), self-host. Schedules prompts across
|
||||
OpenAI, Anthropic, Gemini, Perplexity, Ollama; regex-matches brand
|
||||
mentions; Perplexity Sonar URL capture is unique. Repo:
|
||||
https://github.com/AI2HU/gego — clone, follow README to set API keys
|
||||
and run the cron scheduler.
|
||||
- **llmopt** — open source (Go + React), self-host. Richer multi-pillar
|
||||
scoring (LLM knowledge testing, AEO content scoring, video authority
|
||||
via YouTube transcripts, Reddit authority, search visibility), MCP
|
||||
integration for Claude Code/Desktop. Repo:
|
||||
https://github.com/jonradoff/llmopt — clone, follow README to
|
||||
configure API keys and start the dashboard.
|
||||
- **Manual baseline** — every 2 weeks, run 5 brand queries + 5 category
|
||||
queries against ChatGPT, Claude, Perplexity, Gemini. Log: cited (Y/N),
|
||||
position in answer, sentiment.
|
||||
|
||||
### Content KPIs (per page)
|
||||
|
||||
- GEO Score (this skill's audit)
|
||||
- Citations per AI engine, per query
|
||||
- Position in AI answer (1st sentence, 1st paragraph, body, footer)
|
||||
- Click-through from AI answer (if engine surfaces source links)
|
||||
- Organic traffic to the page (control variable)
|
||||
|
||||
### Brand KPIs
|
||||
|
||||
- Share of Model — % of category-query AI answers mentioning brand
|
||||
- Cross-engine coverage — % of monitored engines citing brand
|
||||
- Sentiment in AI answers — positive / neutral / negative
|
||||
- Wikipedia presence + Wikidata edit recency
|
||||
|
||||
### Monthly review
|
||||
|
||||
- Which content is being cited? Why? (extract the pattern, replicate)
|
||||
- Which content was optimized but isn't cited? Why? (audit fail mode)
|
||||
- Which queries does the brand never appear in? (off-site authority gap?)
|
||||
- Which engines diverge most from the others? (engine-specific playbook
|
||||
not yet running)
|
||||
|
||||
---
|
||||
|
||||
## Quick reference: Do / Don't
|
||||
|
||||
### Do
|
||||
- Front-load the answer in first 150 words
|
||||
- Add real stats with units, sources, dates
|
||||
- Quote real named experts from named publications
|
||||
- Cite ≥1 external source per 500 words from ≥3 source types
|
||||
- Update content every 60 days for competitive queries, 90 days minimum for stable topics
|
||||
- Allow all major AI crawlers in robots.txt + CDN
|
||||
- Add Article + FAQPage + HowTo schema as appropriate
|
||||
- Build Wikipedia / Wikidata / Reddit presence
|
||||
- Track citations across all four major engines
|
||||
- Publish original first-party data quarterly
|
||||
|
||||
### Don't
|
||||
- Fabricate stats, quotes, citations, or expert names
|
||||
- Keyword-stuff (−8% PAWC, actively hurts)
|
||||
- Use vague entities ("experts say", "studies show")
|
||||
- Block AI crawlers in robots.txt or WAF
|
||||
- Ship JS-only content without SSR/SSG
|
||||
- Treat GEO as identical to SEO (different signals, different weights)
|
||||
- Optimize for one engine and assume the others follow
|
||||
- Skip the author byline + sameAs linkage
|
||||
149
skills/graphic-designer/SKILL.md
Normal file
149
skills/graphic-designer/SKILL.md
Normal file
@@ -0,0 +1,149 @@
|
||||
---
|
||||
name: graphic-designer
|
||||
description: >
|
||||
Create LinkedIn post graphics. Decides between an HTML/CSS structured graphic or an AI-generated infographic based on the post content. Use this skill whenever the user says "design a graphic", "create a visual", "make an image", "graphic for my post", "LinkedIn image", or wants any visual content to pair with a LinkedIn post. Also trigger when the user finishes writing a post and wants a matching graphic.
|
||||
---
|
||||
|
||||
# Graphic Designer
|
||||
|
||||
## CRITICAL: Auto-start on load
|
||||
|
||||
When this skill triggers, go straight to Step 1. Do not summarise. Do not explain options. Start immediately.
|
||||
|
||||
## Step 1. Read the post
|
||||
|
||||
Check the project for the most recent post file. If found, read it. If not, say:
|
||||
|
||||
> Paste the post you want a graphic for.
|
||||
|
||||
Wait for the post, then call AskUserQuestion:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"question": "What type of graphic fits this post best?",
|
||||
"header": "Style",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{"label": "HTML/CSS graphic", "description": "Clean structured layout. Framework, comparison, steps, data. Fully editable, screenshot to export."},
|
||||
{"label": "Whiteboard infographic", "description": "Hand-drawn marker style on a whiteboard or notebook page. Recaps the post visually. Generated in Gemini."},
|
||||
{"label": "Branded infographic", "description": "Professional infographic using your brand colours. Recaps the post visually. Generated in Gemini."},
|
||||
{"label": "You decide", "description": "Analyse the post and pick the best format automatically"}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
If "You decide": analyse the post. If it contains numbered steps, frameworks, comparisons, or data tables, go Path A (HTML/CSS). If it recaps a workflow, shares tips, teaches a concept, or tells a story, go Path B (image prompt) and pick whichever style fits better.
|
||||
|
||||
## Path A: HTML/CSS structured graphic
|
||||
|
||||
Design constraints:
|
||||
- 1200 x 1400 pixels (LinkedIn optimal)
|
||||
- Dark background (#1a1a2e or user's brand colour) with high contrast text
|
||||
- Clean sans-serif font (Inter, system-ui)
|
||||
- White or light text on dark background
|
||||
- One accent colour for highlights and dividers
|
||||
- 40px minimum padding on all sides
|
||||
- No stock photo backgrounds
|
||||
- Let the post content dictate how many sections the graphic has. 3 steps = 3 blocks. 10 tips = 10 blocks. The constraint is legibility, not a fixed number. Every element must be large enough to read on a mobile screen.
|
||||
|
||||
Single self-contained HTML file with inline CSS. Include viewport meta tag.
|
||||
|
||||
Extract the core framework or steps from the post. Do not copy the full post. Distil into:
|
||||
- A short headline (5 to 8 words)
|
||||
- Key points as visual blocks (Unicode icons fine)
|
||||
- Footer with author name from about-me.md if available
|
||||
|
||||
Save the HTML file. Tell the user:
|
||||
|
||||
> Open the HTML in your browser and screenshot it.
|
||||
|
||||
## Path B: Image generation prompt
|
||||
|
||||
The graphic must recap the post content visually. It is not an abstract illustration or stock photo. It summarises the key information from the post in a visual format the reader can scan.
|
||||
|
||||
First, extract the content for the infographic from the post:
|
||||
- The main headline or hook (shortened to 5 to 10 words)
|
||||
- 3 to 6 key points, steps, or takeaways (one short line each)
|
||||
- Any numbers, stats, or data worth highlighting
|
||||
- A footer line (author name and CTA if appropriate)
|
||||
|
||||
Then build the prompt based on the chosen style.
|
||||
|
||||
### Style 1: Whiteboard infographic
|
||||
|
||||
Use this prompt template. Fill in the content sections from the post.
|
||||
|
||||
```
|
||||
Generate a single image of a physical, hand-drawn infographic on a large whiteboard or notebook page.
|
||||
|
||||
Crucial Style Instructions (Read First):
|
||||
Medium: The image must look like a photograph of a real whiteboard or large paper notepad.
|
||||
Texture: All elements must look created by hand using colored marker pens (black, blue, red, green) and highlighters (yellow/orange). Lines should be slightly imperfect, wobbly, and have the texture of ink on a surface.
|
||||
No Digital Fonts: All text, headings, and bullet points must appear handwritten or hand-printed in marker pen.
|
||||
|
||||
Layout: Structure the 1080x1350 image as follows:
|
||||
|
||||
TITLE (large, bold marker, top of page):
|
||||
[Insert headline from the post]
|
||||
|
||||
CONTENT (hand-drawn sections with marker pen):
|
||||
[Insert 3 to 6 key points, each as a short hand-written line with a bullet, number, or small icon drawn next to it]
|
||||
|
||||
[If there are stats or numbers, draw them large with a circle or box around them]
|
||||
|
||||
Use multi-colored markers for emphasis. Keep text large and legible. Make everything look hand-drawn with slight imperfections. Make it look like a photograph of an actual notebook page.
|
||||
|
||||
Always include the handwritten text "[Author name from about-me.md] | Repost" at the bottom of the image, in the same hand-drawn marker style.
|
||||
```
|
||||
|
||||
### Style 2: Branded infographic
|
||||
|
||||
Ask the user for brand colours if not already known. If about-me.md exists, check there first.
|
||||
|
||||
```
|
||||
Generate a professional infographic image at 1080x1350 pixels.
|
||||
|
||||
Style: Clean, modern, editorial. Flat design with sharp edges and strong typography. No 3D effects, no gradients, no stock photos.
|
||||
|
||||
Colour palette:
|
||||
- Background: [primary brand colour or dark neutral]
|
||||
- Text: [white or high-contrast colour]
|
||||
- Accent: [secondary brand colour]
|
||||
|
||||
Layout:
|
||||
HEADLINE (top, large bold text):
|
||||
[Insert headline from the post]
|
||||
|
||||
BODY (structured sections, each with an icon or number):
|
||||
[Insert 3 to 6 key points as short lines, each with a visual marker: numbered circle, checkmark, or simple icon]
|
||||
|
||||
[If there are stats, display them as large feature numbers with a label underneath]
|
||||
|
||||
FOOTER:
|
||||
[Author name from about-me.md] | [CTA or tagline if appropriate]
|
||||
|
||||
Keep text large and scannable. Maximum 40 words on the entire image. No decorative borders. No watermarks. No logos unless the user provides one.
|
||||
```
|
||||
|
||||
Output the complete prompt in a code block. Tell the user:
|
||||
|
||||
> Paste this into Gemini or your image generator. The prompt is ready to go.
|
||||
|
||||
## After either path
|
||||
|
||||
Say:
|
||||
|
||||
> Graphic ready. Say "score my post" when you want feedback before publishing.
|
||||
|
||||
## Rules
|
||||
|
||||
- Always read the post before designing. The graphic must recap the post content, not illustrate an abstract concept.
|
||||
- Structured graphics (Path A) must be a single HTML file with inline CSS.
|
||||
- Image prompts (Path B) must be fully self-contained. The user pastes it cold into Gemini and gets the graphic.
|
||||
- Extract and distil the post content into the graphic. No copying the full post text.
|
||||
- Whiteboard style: always hand-drawn marker look, imperfect lines, coloured pens, notebook/whiteboard texture.
|
||||
- Branded style: always clean, flat, modern, using the user's brand colours.
|
||||
- Never use em dashes in any output.
|
||||
- British English throughout.
|
||||
89
skills/hook-generator/SKILL.md
Normal file
89
skills/hook-generator/SKILL.md
Normal file
@@ -0,0 +1,89 @@
|
||||
---
|
||||
name: hook-generator
|
||||
description: >
|
||||
Generate 6 clickbait-style LinkedIn hook variations for any topic. Two-line hooks built on the formula: a 40-char opening line, a 40-char bold contrast line. Includes digits, "How I" or "I" statements, and metrics. Use this skill whenever the user says "write me hooks", "hook ideas", "generate hooks", "I need a hook for a post about...", or pastes a topic and asks for openers. Fast output, no preamble.
|
||||
---
|
||||
|
||||
# Hook Generator
|
||||
|
||||
## CRITICAL: Auto-start on load
|
||||
|
||||
When this skill triggers, go straight to Step 1. Do not summarise. Do not explain what makes a good hook.
|
||||
|
||||
## Step 1. Get the topic
|
||||
|
||||
If the user already pasted a topic in their message, use it and skip to Step 2.
|
||||
|
||||
Otherwise ask:
|
||||
|
||||
> What topic do you want hooks for?
|
||||
|
||||
Wait for response.
|
||||
|
||||
## Step 2. Write 6 hook variations
|
||||
|
||||
Every hook has the same structure:
|
||||
|
||||
- **Line 1 (Opening)**: 40 characters maximum. No questions. States something unexpected, specific, or punchy.
|
||||
- **Line 2 (Contrast)**: 40 characters maximum. Contradicts, reframes, or undercuts the opening.
|
||||
|
||||
Every variation must:
|
||||
|
||||
- Include at least one "How I" or "I" statement across the two lines
|
||||
- Include a digit or metric where possible
|
||||
- Follow clickbait principles: tension, curiosity gap, stakes
|
||||
|
||||
Produce 6 variations covering different angles:
|
||||
|
||||
1. **Number-led**: Lead with a specific number or metric
|
||||
2. **Contrarian**: State a belief then flip it
|
||||
3. **Personal transformation**: Before vs after with a digit
|
||||
4. **Authority steal**: Reference a name, tool, or brand
|
||||
5. **Admission**: Confess a mistake or loss
|
||||
6. **Future shock**: A prediction or "X is about to change"
|
||||
|
||||
## Step 3. Output format
|
||||
|
||||
```
|
||||
HOOKS for [topic]
|
||||
|
||||
1. [Number-led]
|
||||
[Line 1]
|
||||
[Line 2]
|
||||
|
||||
2. [Contrarian]
|
||||
[Line 1]
|
||||
[Line 2]
|
||||
|
||||
3. [Personal transformation]
|
||||
[Line 1]
|
||||
[Line 2]
|
||||
|
||||
4. [Authority steal]
|
||||
[Line 1]
|
||||
[Line 2]
|
||||
|
||||
5. [Admission]
|
||||
[Line 1]
|
||||
[Line 2]
|
||||
|
||||
6. [Future shock]
|
||||
[Line 1]
|
||||
[Line 2]
|
||||
```
|
||||
|
||||
## Step 4. Offer the next move
|
||||
|
||||
Ask:
|
||||
|
||||
> Want me to build one of these into a full post? Call the post-formatter skill with the hook number.
|
||||
|
||||
## Rules
|
||||
|
||||
- 40 characters maximum per line. Count them.
|
||||
- No questions in the opening line.
|
||||
- No em dashes.
|
||||
- No filler words. Every word earns its place.
|
||||
- Prefer digits over spelled numbers (3, not three).
|
||||
- British English unless voice.md says otherwise.
|
||||
- Never hedge. A weak hook is worse than no hook.
|
||||
228
skills/mm-analytics-orchestrator/SKILL.md
Normal file
228
skills/mm-analytics-orchestrator/SKILL.md
Normal file
@@ -0,0 +1,228 @@
|
||||
---
|
||||
name: mm-analytics-orchestrator
|
||||
description: >
|
||||
Pull SEO + Ad performance data for a client, analyze trends, and produce
|
||||
prioritized strategy recommendations. Saves report to vault and presents
|
||||
in chat. Use when: "วิเคราะห์ผล", "analytics report", "ดูสถิติ",
|
||||
"แนะนำกลยุทธ์", "strategy recommendations", "seo report", "ad performance",
|
||||
"content ideas from data", "เขียนอะไรดี", "analytics".
|
||||
---
|
||||
|
||||
# mm-analytics-orchestrator
|
||||
|
||||
Pulls analytics data, analyzes trends, and produces prioritized strategy
|
||||
recommendations for a client.
|
||||
|
||||
## When This Must Trigger
|
||||
|
||||
- "วิเคราะห์ผล", "analytics report", "ดูสถิติ"
|
||||
- "แนะนำกลยุทธ์", "strategy recommendations"
|
||||
- "seo report", "ad performance"
|
||||
- "content ideas from data", "เขียนอะไรดี"
|
||||
- "analytics"
|
||||
|
||||
## Process
|
||||
|
||||
### Step 1: Determine client
|
||||
|
||||
If not specified, ask:
|
||||
|
||||
```
|
||||
วิเคราะห์ผลของลูกค้าไหน?
|
||||
|
||||
1. moreminimore
|
||||
2. [other clients...]
|
||||
```
|
||||
|
||||
### Step 2: Pull data
|
||||
|
||||
Run analytics scripts from `~/Gitea/moreminimore-service-system/scripts/`:
|
||||
|
||||
```bash
|
||||
# GSC data
|
||||
python3 mm_gsc.py --client <client-id> --days 30 --json
|
||||
|
||||
# Google Ads data (if available)
|
||||
python3 mm_google_ads.py --client <client-id> stats --days 30 --json
|
||||
|
||||
# Meta Ads data (if available)
|
||||
python3 mm_meta_ads.py --client <client-id> stats --days 30 --json
|
||||
```
|
||||
|
||||
If a script fails (no data, API error), skip that source and note it.
|
||||
|
||||
### Step 3: Analyze
|
||||
|
||||
#### SEO Analysis
|
||||
|
||||
**a. Quick Wins (Position 4-10)**
|
||||
- Queries on page 1 but below top 3
|
||||
- High impression, low CTR → title/description optimization
|
||||
- Action: ปรับ meta title/description
|
||||
|
||||
**b. Content Gaps (Position 11-30)**
|
||||
- Queries with impressions but no dedicated content
|
||||
- Action: เขียนบทความใหม่
|
||||
|
||||
**c. CTR Opportunities**
|
||||
- High impressions, low CTR (< expected for position)
|
||||
- Action: ปรับ title/description, add schema
|
||||
|
||||
**d. Declining Pages**
|
||||
- Pages with >30% traffic decline
|
||||
- Action: Refresh content, update data
|
||||
|
||||
**e. GEO Opportunities**
|
||||
- Question-format queries, definition queries
|
||||
- Action: Optimize for AI citation
|
||||
|
||||
#### Ad Analysis (if data available)
|
||||
|
||||
**a. High CPA Campaigns**
|
||||
- CPA > 2x target
|
||||
- Action: ปรับ targeting, ทดสอบ creative ใหม่
|
||||
|
||||
**b. Low QS Keywords**
|
||||
- Quality Score < 5
|
||||
- Action: ปรับ ad relevance, landing page
|
||||
|
||||
**c. Budget Optimization**
|
||||
- High spend, low conversion
|
||||
- Action: ย้าย budget
|
||||
|
||||
**d. Creative Fatigue**
|
||||
- CTR declining over time
|
||||
- Action: Refresh creative
|
||||
|
||||
#### Cross-Channel
|
||||
|
||||
**a. SEO + Ad Synergy**
|
||||
- Organic ranking well for paid keyword
|
||||
- Action: ลด paid spend
|
||||
|
||||
**b. Content → Ad Pipeline**
|
||||
- Article with good traffic → create ad
|
||||
- Action: สร้าง campaign จาก content
|
||||
|
||||
**c. Ad → Content Pipeline**
|
||||
- Keyword with good ad conversion → write article
|
||||
- Action: เขียนบทความ SEO
|
||||
|
||||
### Step 4: Prioritize recommendations
|
||||
|
||||
Rank by impact × effort:
|
||||
|
||||
| Priority | Criteria |
|
||||
|----------|----------|
|
||||
| 🔴 Priority 1 | High impact, low effort (quick wins) |
|
||||
| 🟡 Priority 2 | High impact, medium effort (content opportunities) |
|
||||
| 🟢 Priority 3 | Medium impact, optimization (ad tuning) |
|
||||
| 🔵 Priority 4 | Cross-channel opportunities |
|
||||
|
||||
### Step 5: Present and ask
|
||||
|
||||
Present the report in chat (match user's language):
|
||||
|
||||
```
|
||||
📋 Strategy Report: [Client Name]
|
||||
📅 Period: [date range]
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
📊 SEO Performance
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
| Metric | Value | Trend |
|
||||
|--------|-------|-------|
|
||||
| Clicks | X | ↑/↓ X% |
|
||||
| Impressions | X | ↑/↓ X% |
|
||||
| CTR | X% | ↑/↓ |
|
||||
| Position | X | ↑/↓ |
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
🎯 Priority 1: Quick Wins
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
1. [Action]
|
||||
Evidence: [data]
|
||||
Expected: [outcome]
|
||||
|
||||
2. [Action]
|
||||
...
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
📝 Priority 2: Content Opportunities
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
1. เขียนบทความ: "[topic]"
|
||||
Keyword: [kw], [X] impressions, position [X]
|
||||
Search intent: [type]
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
📢 Priority 3: Ad Optimization
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
(ถ้ามีข้อมูล ads)
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
🔄 Priority 4: Cross-Channel
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
```
|
||||
|
||||
Then ask:
|
||||
|
||||
```
|
||||
อยากทำตามแนะนำไหม?
|
||||
|
||||
1. ✅ ทำทั้งหมด — สร้าง brief / optimize ตามลำดับ
|
||||
2. 📝 เฉพาะ content — สร้าง brief จาก content gaps
|
||||
3. 📢 เฉพาะ ads — optimize campaigns
|
||||
4. ⏭️ ข้าม — แค่ดูรายงาน
|
||||
```
|
||||
|
||||
If user chooses to act:
|
||||
- Content gaps → load `mm-article-idea-extract` with the recommended topic
|
||||
- Ad optimization → provide specific commands/changes
|
||||
- SEO fixes → provide specific title/description rewrites
|
||||
|
||||
### Step 6: Save report
|
||||
|
||||
Save to vault:
|
||||
|
||||
```
|
||||
~/vault/50_Resources/Strategy for Customers/<client-id>/YYYY-MM-DD-report.md
|
||||
```
|
||||
|
||||
Create the folder if it doesn't exist.
|
||||
|
||||
Report format:
|
||||
|
||||
```markdown
|
||||
---
|
||||
type: strategy-report
|
||||
client: <client-id>
|
||||
period: <start-date> to <end-date>
|
||||
generated: YYYY-MM-DD
|
||||
---
|
||||
|
||||
# Strategy Report: [Client Name]
|
||||
[full report content]
|
||||
```
|
||||
|
||||
### Step 7: Confirm
|
||||
|
||||
```
|
||||
✅ รายงาน saved: ~/vault/50_Resources/Strategy for Customers/<client-id>/YYYY-MM-DD-report.md
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
- Chat: full report with recommendations
|
||||
- Vault: `~/vault/50_Resources/Strategy for Customers/<client-id>/YYYY-MM-DD-report.md`
|
||||
|
||||
## Notes
|
||||
|
||||
- Match user's language (Thai if they write Thai, English if English)
|
||||
- If GSC data is empty (new site), note it and focus on ad data
|
||||
- If no ad data, focus on SEO only
|
||||
- Always ask before taking action (creating briefs, optimizing)
|
||||
- Reports are cumulative — each report builds on previous data
|
||||
213
skills/mm-analytics/SKILL.md
Normal file
213
skills/mm-analytics/SKILL.md
Normal file
@@ -0,0 +1,213 @@
|
||||
---
|
||||
name: mm-analytics
|
||||
description: >
|
||||
Analyze website SEO performance (Google Search Console) and ad campaign
|
||||
performance (Google Ads, Meta Ads) for a client. Produces insights and
|
||||
recommendations for content and marketing strategy.
|
||||
Use when: "analytics", "วิเคราะห์", "ดูสถิติ", "performance report",
|
||||
"seo report", "ad performance", "mm analytics", "ดูผล".
|
||||
---
|
||||
|
||||
# mm-analytics
|
||||
|
||||
Analyzes SEO and ad performance for a client, produces insights and recommendations.
|
||||
|
||||
## When This Must Trigger
|
||||
|
||||
- "analytics", "วิเคราะห์", "ดูสถิติ", "performance report"
|
||||
- "seo report", "ad performance", "mm analytics", "ดูผล"
|
||||
- "วิเคราะห์เว็บ", "ดู traffic", "ดู campaign"
|
||||
|
||||
## Scripts Location
|
||||
|
||||
All scripts are in `~/Gitea/moreminimore-service-system/scripts/`:
|
||||
- `mm_gsc.py` — Google Search Console analytics
|
||||
- `mm_google_ads.py` — Google Ads analytics
|
||||
- `mm_meta_ads.py` — Meta Ads analytics
|
||||
|
||||
## Process
|
||||
|
||||
### Step 1: Determine client and analysis type
|
||||
|
||||
```
|
||||
ต้องการวิเคราะห์อะไร?
|
||||
|
||||
1. 🌐 SEO (Google Search Console) — traffic, queries, rankings
|
||||
2. 📢 Google Ads — campaign performance, keywords
|
||||
3. 📱 Meta Ads — Facebook/Instagram ad performance
|
||||
4. 📊 ทั้งหมด — comprehensive report
|
||||
```
|
||||
|
||||
### Step 2: Run analytics scripts
|
||||
|
||||
#### 2a. SEO Analytics (GSC)
|
||||
|
||||
```bash
|
||||
cd ~/Gitea/moreminimore-service-system/scripts
|
||||
python3 mm_gsc.py --client <client-id> --days 90
|
||||
```
|
||||
|
||||
Output:
|
||||
- Top queries by impressions, clicks, CTR
|
||||
- Top pages by traffic
|
||||
- Position distribution (top 3, 4-10, 11-20, 21+)
|
||||
- Traffic trends (increasing/decreasing)
|
||||
- Content gap opportunities
|
||||
|
||||
#### 2b. Google Ads Analytics
|
||||
|
||||
```bash
|
||||
cd ~/Gitea/moreminimore-service-system/scripts
|
||||
python3 mm_google_ads.py --client <client-id> campaigns
|
||||
python3 mm_google_ads.py --client <client-id> keywords
|
||||
python3 mm_google_ads.py --client <client-id> stats --days 30
|
||||
```
|
||||
|
||||
Output:
|
||||
- Active campaigns with performance metrics
|
||||
- Keyword performance (impressions, clicks, CTR, CPC, QS)
|
||||
- Campaign stats with cost per conversion
|
||||
|
||||
#### 2c. Meta Ads Analytics
|
||||
|
||||
```bash
|
||||
cd ~/Gitea/moreminimore-service-system/scripts
|
||||
python3 mm_meta_ads.py --client <client-id> campaigns
|
||||
python3 mm_meta_ads.py --client <client-id> stats --days 30
|
||||
```
|
||||
|
||||
Output:
|
||||
- Active campaigns with objectives and budgets
|
||||
- Performance stats (impressions, clicks, spend, reach, CTR, CPC)
|
||||
|
||||
### Step 3: Analyze and produce insights
|
||||
|
||||
After collecting data, analyze for:
|
||||
|
||||
**SEO Insights:**
|
||||
- Queries with high impressions but low CTR → title/description optimization
|
||||
- Queries ranking 4-10 → quick win opportunities
|
||||
- Pages losing traffic → content refresh needed
|
||||
- Content gaps → new article topics
|
||||
- GEO opportunities → AI search optimization
|
||||
|
||||
**Ad Insights:**
|
||||
- Campaigns with high CPA → optimize targeting/creative
|
||||
- Keywords with low QS → improve ad relevance
|
||||
- Ad fatigue signals → refresh creative
|
||||
- Budget allocation → shift spend to better performers
|
||||
|
||||
**Cross-channel Insights:**
|
||||
- SEO queries that match ad keywords → organic vs paid synergy
|
||||
- Content topics that perform well in ads → create organic content
|
||||
- Landing page performance → conversion optimization
|
||||
|
||||
### Step 4: Generate recommendations
|
||||
|
||||
Produce actionable recommendations:
|
||||
|
||||
```
|
||||
📋 Analytics Report: [Client Name]
|
||||
📅 Period: [date range]
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
🌐 SEO Performance
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
Top Queries: [list]
|
||||
Top Pages: [list]
|
||||
Traffic Trend: [↑/↓/→]
|
||||
|
||||
⚡ Quick Wins:
|
||||
1. [Query] — position [X], [Y] impressions → optimize title
|
||||
2. [Page] — losing traffic → refresh content
|
||||
|
||||
📝 Content Opportunities:
|
||||
1. [Topic] — high search volume, no content → write article
|
||||
2. [Topic] — ranking 11-20 → expand existing content
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
📢 Google Ads Performance
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
Campaigns: [N] active
|
||||
Total Spend: ฿[X]
|
||||
Avg CPA: ฿[X]
|
||||
ROAS: [X]x
|
||||
|
||||
⚡ Recommendations:
|
||||
1. [Campaign] — high CPA → adjust targeting
|
||||
2. [Keyword] — low QS → improve ad relevance
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
📱 Meta Ads Performance
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
Campaigns: [N] active
|
||||
Total Spend: ฿[X]
|
||||
Reach: [X]
|
||||
CTR: [X]%
|
||||
|
||||
⚡ Recommendations:
|
||||
1. [Campaign] — low CTR → refresh creative
|
||||
2. [Ad Set] — high frequency → new audience
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
📋 Next Actions
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
Content:
|
||||
1. Write article about [topic] (from SEO gap)
|
||||
2. Refresh [page] (losing traffic)
|
||||
|
||||
Ads:
|
||||
1. Optimize [campaign] (high CPA)
|
||||
2. Refresh [ad creative] (fatigue)
|
||||
|
||||
Strategy:
|
||||
1. [Recommendation based on cross-channel data]
|
||||
```
|
||||
|
||||
### Step 5: Save report
|
||||
|
||||
Save analytics report to:
|
||||
```
|
||||
~/vault/60_Articles/<client-id>/analytics/YYYY-MM-DD-report.md
|
||||
```
|
||||
|
||||
## Integration with Content Pipeline
|
||||
|
||||
The analytics feed back into the content pipeline:
|
||||
|
||||
1. **SEO gaps** → become new article topics (mm-article-idea-extract)
|
||||
2. **Top performing content** → inform future content strategy
|
||||
3. **Ad performance** → inform CTA and messaging for ad copy
|
||||
4. **Cross-channel insights** → optimize overall marketing strategy
|
||||
|
||||
## Error Handling
|
||||
|
||||
If a script fails:
|
||||
- Check if credentials are configured in clients-config.json
|
||||
- Check if the API is enabled (Google Ads API, Meta Marketing API)
|
||||
- Check if the access token is valid
|
||||
|
||||
```
|
||||
❌ [Script] failed: [error]
|
||||
|
||||
สาเหตุที่เป็นไปได้:
|
||||
- Credentials ไม่ถูกต้อง → ตรวจสอบ clients-config.json
|
||||
- API ไม่ได้เปิดใช้งาน → เปิดใช้ใน Google Cloud / Meta Developer
|
||||
- Token หมดอายุ → ต่ออายุ token
|
||||
|
||||
ต้องการ:
|
||||
1. ลองใหม่
|
||||
2. ข้าม → วิเคราะห์เฉพาะส่วนที่ใช้ได้
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- All scripts read from `~/vault/99_System/clients-config.json`
|
||||
- GSC uses service account key from `global.google.service_account_key`
|
||||
- Google Ads uses developer_token + gcloud ADC for authentication
|
||||
- Meta Ads uses page_token or system_user_token from config
|
||||
- Reports are saved to the client's vault folder for reference
|
||||
310
skills/mm-article-from-research/SKILL.md
Normal file
310
skills/mm-article-from-research/SKILL.md
Normal file
@@ -0,0 +1,310 @@
|
||||
---
|
||||
name: mm-article-from-research
|
||||
description: >
|
||||
Data Stage A: Convert a vault research note into an article brief.
|
||||
Scans 30_Research/ for resources not yet used for articles, lets user pick one,
|
||||
reads the content, summarizes key points, asks user which angles to focus on,
|
||||
then creates a brief.md for the content-writer-mm skill.
|
||||
Use when: "เขียนบทความจาก research", "article from research", "ใช้ research เขียนบทความ".
|
||||
---
|
||||
|
||||
# Article From Research
|
||||
|
||||
Converts existing vault research notes into structured article briefs.
|
||||
|
||||
## When This Must Trigger
|
||||
|
||||
- "เขียนบทความจาก research", "article from research"
|
||||
- "ใช้ research เขียนบทความ", "make article from research"
|
||||
- "เลือก research มาเขียน"
|
||||
- "ทั้งหมด", "ทุก research", "batch" (batch mode — see Step 1b)
|
||||
|
||||
## Process
|
||||
|
||||
### Step 1: Determine mode — Single or Batch
|
||||
|
||||
Ask the user if they want to work on ONE research or ALL at once:
|
||||
|
||||
```python
|
||||
# Detect batch intent
|
||||
batch_keywords = ['ทั้งหมด', 'ทุก', 'all', 'batch', 'ทุกไฟล์', '14', '13', '12',
|
||||
'ทุก research', '14 บทความ', 'ทั้งหมดเลย']
|
||||
user_msg = user_message.lower()
|
||||
is_batch = any(kw in user_msg for kw in batch_keywords)
|
||||
```
|
||||
|
||||
- **Single mode** (default): user picks one research → interactive focus questions
|
||||
- **Batch mode**: process ALL unused research at once → skip interactivity
|
||||
|
||||
Trigger batch immediately if user says "ทั้งหมด" or "ทุก research" or "ทุกไฟล์"
|
||||
|
||||
### Step 0: Determine client
|
||||
|
||||
Ask which client this article is for:
|
||||
|
||||
```
|
||||
บทความนี้สำหรับลูกค้าไหน?
|
||||
|
||||
1. [client-1] — [display_name]
|
||||
2. [client-2] — [display_name]
|
||||
```
|
||||
|
||||
If no clients configured, suggest running `mm-article-idea-extract` to add one first.
|
||||
|
||||
### Step 1: Scan 30_Research/<client-id>/ for available resources
|
||||
|
||||
```bash
|
||||
VAULT_PATH="$HOME/vault"
|
||||
```
|
||||
|
||||
Search for research notes in both client-specific and general folders that have
|
||||
NOT been marked as used for articles:
|
||||
|
||||
```python
|
||||
# Search 30_Research/<client-id>/ AND 30_Research/general/
|
||||
# Exclude Archive/ subfolders and notes with article_created: true
|
||||
import os, re
|
||||
|
||||
vault = os.path.expanduser("~/vault/30_Research")
|
||||
results = []
|
||||
search_dirs = [
|
||||
os.path.join(vault, client_id), # client-specific
|
||||
os.path.join(vault, "general"), # general research
|
||||
]
|
||||
|
||||
for search_dir in search_dirs:
|
||||
if not os.path.exists(search_dir):
|
||||
continue
|
||||
for root, dirs, files in os.walk(search_dir):
|
||||
# Skip Archive folders
|
||||
if 'Archive' in root.split(os.sep):
|
||||
continue
|
||||
for f in files:
|
||||
if not f.endswith('.md'):
|
||||
continue
|
||||
path = os.path.join(root, f)
|
||||
try:
|
||||
content = open(path).read()
|
||||
if 'article_created: true' in content[:500]:
|
||||
continue
|
||||
title_match = re.search(r'^#\s+(.+)$', content, re.MULTILINE)
|
||||
title = title_match.group(1) if title_match else f.replace('.md', '')
|
||||
type_match = re.search(r'^type:\s*(.+)$', content, re.MULTILINE)
|
||||
note_type = type_match.group(1).strip() if type_match else 'unknown'
|
||||
results.append({
|
||||
'path': path,
|
||||
'title': title,
|
||||
'type': note_type,
|
||||
'relative': os.path.relpath(path, os.path.expanduser("~/vault"))
|
||||
})
|
||||
except:
|
||||
continue
|
||||
```
|
||||
|
||||
Present results to user as a numbered list:
|
||||
|
||||
```
|
||||
พบ research notes ที่ยังไม่ได้ใช้เขียนบทความ:
|
||||
|
||||
1. [Title] — type: reference — 30_Research/Area/Topic/Topic.md
|
||||
2. [Title] — type: reference — 30_Research/Area/Topic2/Topic2.md
|
||||
...
|
||||
|
||||
เลือกหมายเลขที่ต้องการเขียนบทความ (หรือพิมพ์ชื่อหัวข้อเพื่อค้นหา):
|
||||
```
|
||||
|
||||
If no research notes found, suggest running `article-idea-extract` instead.
|
||||
|
||||
### Step 1b: Batch mode — Process ALL research at once
|
||||
|
||||
Skip interactive selection. Read ALL research notes from 30_Research/ that don't have `article_created: true`.
|
||||
|
||||
Classify into two buckets:
|
||||
|
||||
| Status | Action |
|
||||
|--------|--------|
|
||||
| `reference`, `candidate`, `draft` | Ready — create brief from existing content |
|
||||
| `seed` | **Research first** — search the web for current info, stats, examples, and sources. Compile into a supplementary data block for the brief |
|
||||
|
||||
For seed-status notes, search internet for:
|
||||
- Latest articles/tutorials on the topic
|
||||
- Official documentation or product pages
|
||||
- Statistics, benchmarks, or case studies
|
||||
- Competitor or comparison info
|
||||
|
||||
### Step 1c: Apply date convention (ALL modes)
|
||||
|
||||
**Article date = source research date.** Extract `date:` from research frontmatter:
|
||||
|
||||
```python
|
||||
date_m = re.search(r'^date:\s*(.+)$', content, re.MULTILINE)
|
||||
article_date = date_m.group(1).strip() if date_m else today
|
||||
```
|
||||
|
||||
Use this date for:
|
||||
- Article folder name: `~/vault/60_Articles/<article_date>-<slug>/`
|
||||
- Article frontmatter `date:` field
|
||||
- DO NOT use today's date unless the research has no date field at all
|
||||
|
||||
### Step 2: Read the selected research note (Single mode)
|
||||
|
||||
Read the full content of the selected research note using `read_file`.
|
||||
|
||||
Extract:
|
||||
- **Title** — from H1
|
||||
- **Key concepts** — from H2/H3 headings
|
||||
- **Main points** — from body content
|
||||
- **Related notes** — from wikilinks `[[...]]`
|
||||
- **Area** — from frontmatter `area:` field
|
||||
- **Tags** — from frontmatter `tags:` field
|
||||
|
||||
### Step 3: Summarize and ask for focus
|
||||
|
||||
Present a summary of the research:
|
||||
|
||||
```
|
||||
📄 Research: [Title]
|
||||
📂 Area: [Area]
|
||||
🏷️ Tags: [tags]
|
||||
|
||||
สรุปประเด็นหลัก:
|
||||
1. [Key point 1]
|
||||
2. [Key point 2]
|
||||
3. [Key point 3]
|
||||
...
|
||||
|
||||
บทความนี้ควรเน้นประเด็นไหนบ้าง?
|
||||
(เลือกหมายเลข หรือพิมพ์ประเด็นเพิ่มเติม)
|
||||
```
|
||||
|
||||
### Step 4: Iterate on details
|
||||
|
||||
After user selects focus areas, ask follow-up questions:
|
||||
|
||||
- มีมุมมองเฉพาะที่อยากเพิ่มไหม?
|
||||
- กลุ่มเป้าหมายของบทความคือใคร?
|
||||
- โทนบทความ: ทางการ / casual / technical?
|
||||
- ความยาวที่ต้องการ: สั้น (800 words) / กลาง (1500) / ยาว (2000+)?
|
||||
- ต้องการ SEO keyword ไหม? ถ้ามี คืออะไร?
|
||||
|
||||
### Step 5: Additional research (optional)
|
||||
|
||||
If the research note is thin or user wants more depth:
|
||||
|
||||
1. Search internet for current information on the topic
|
||||
2. Look for statistics, examples, case studies
|
||||
3. Find expert quotes or references
|
||||
4. Check what competitors have written (SERP analysis)
|
||||
|
||||
### Step 6: Create brief.md
|
||||
|
||||
Create the article brief at:
|
||||
|
||||
```
|
||||
~/vault/60_Articles/<client-id>/YYYY-MM-DD-<slug>/brief.md
|
||||
```
|
||||
|
||||
Where `<slug>` is derived from the topic (kebab-case, English).
|
||||
|
||||
```markdown
|
||||
---
|
||||
type: brief
|
||||
status: ready
|
||||
created: YYYY-MM-DD
|
||||
source_research: "[[Original Research Title]]"
|
||||
source_path: "30_Research/Area/Topic/Topic.md"
|
||||
---
|
||||
|
||||
# Article Brief: [Topic]
|
||||
|
||||
## Source
|
||||
- Research: [[Original Research Title]]
|
||||
- Path: 30_Research/Area/Topic/Topic.md
|
||||
|
||||
## Target
|
||||
- Audience: [who]
|
||||
- Tone: [formal/casual/technical]
|
||||
- Length: [target word count]
|
||||
- SEO Keyword: [if provided]
|
||||
|
||||
## Key Angles
|
||||
1. [Angle 1]
|
||||
2. [Angle 2]
|
||||
3. [Angle 3]
|
||||
|
||||
## Outline
|
||||
### H1: [Proposed title]
|
||||
### H2: [Section 1]
|
||||
- [Key point]
|
||||
### H2: [Section 2]
|
||||
- [Key point]
|
||||
### H2: [Section 3]
|
||||
- [Key point]
|
||||
|
||||
## Research Data
|
||||
[Key statistics, quotes, facts from the research note + internet research]
|
||||
|
||||
## Related Content
|
||||
- [[Related Note 1]]
|
||||
- [[Related Note 2]]
|
||||
|
||||
## Content Requirements
|
||||
- Featured image: [description]
|
||||
- Inline images: [list with descriptions]
|
||||
- FAQ section: [yes/no, topics if yes]
|
||||
- Schema markup: [Article/BlogPosting]
|
||||
```
|
||||
|
||||
### Step 7: Mark research note as used AND move to Archive
|
||||
|
||||
Update the original research note's frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
...existing frontmatter...
|
||||
article_created: true
|
||||
article_link: "[[YYYY-MM-DD-slug]]"
|
||||
---
|
||||
```
|
||||
|
||||
Use `patch` to add the two fields to the frontmatter.
|
||||
|
||||
Then move the research note to the client's Archive folder:
|
||||
|
||||
```bash
|
||||
# Determine source location
|
||||
SOURCE_DIR=$(dirname "$RESEARCH_PATH")
|
||||
ARCHIVE_DIR="$SOURCE_DIR/Archive"
|
||||
|
||||
# Create Archive if it doesn't exist
|
||||
mkdir -p "$ARCHIVE_DIR"
|
||||
|
||||
# Move the file
|
||||
mv "$RESEARCH_PATH" "$ARCHIVE_DIR/$(basename $RESEARCH_PATH)"
|
||||
```
|
||||
|
||||
**Important**: Only move if the research is in a client-specific folder.
|
||||
If the research is in `30_Research/general/`, mark as used but DON'T move
|
||||
(general research can be reused for other clients).
|
||||
|
||||
### Step 8: Confirm
|
||||
|
||||
```
|
||||
✅ Brief created: ~/vault/60_Articles/<client-id>/YYYY-MM-DD-slug/brief.md
|
||||
✅ Research marked: article_created: true
|
||||
✅ Research archived: moved to 30_Research/<client-id>/Archive/
|
||||
|
||||
ถัดไป: โหลด mm-content-writer skill เพื่อเริ่มเขียนบทความ
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
- `~/vault/60_Articles/<client-id>/<date>-<slug>/brief.md` — Article brief ready for mm-content-writer
|
||||
- Updated frontmatter in `30_Research/<client-id>/` source note
|
||||
- Source note moved to `30_Research/<client-id>/Archive/` (if client-specific)
|
||||
|
||||
## Notes
|
||||
|
||||
- If user can't find what they want in 30_Research/, suggest `article-idea-extract` skill
|
||||
- The brief is the contract between Data Stage and Content Stage — be thorough
|
||||
- Always preserve the wikilink to the source research
|
||||
368
skills/mm-article-idea-extract/SKILL.md
Normal file
368
skills/mm-article-idea-extract/SKILL.md
Normal file
@@ -0,0 +1,368 @@
|
||||
---
|
||||
name: mm-article-idea-extract
|
||||
description: >
|
||||
Data Stage B: Take an article idea, research it thoroughly (vault + internet),
|
||||
iterate with the user on focus areas and angles, then produce a complete brief.md.
|
||||
Use when: "มีไอเดียบทความ", "article idea", "อยากเขียนเรื่อง", "extract article idea",
|
||||
"prepare article topic", "เตรียมหัวข้อเขียนบทความ".
|
||||
---
|
||||
|
||||
# Article Idea Extract & Prepare
|
||||
|
||||
Takes an article idea, researches it deeply, iterates with the user, and produces
|
||||
a complete article brief ready for the content writer.
|
||||
|
||||
## When This Must Trigger
|
||||
|
||||
- "มีไอเดียบทความ", "article idea", "อยากเขียนเรื่อง"
|
||||
- "extract article idea", "prepare article topic"
|
||||
- "เตรียมหัวข้อเขียนบทความ", "research ก่อนเขียน"
|
||||
- User provides a topic/keyword/concept and wants to write about it
|
||||
|
||||
## Client Onboarding (when adding new client)
|
||||
|
||||
When user selects "ลูกค้าใหม่", run this flow:
|
||||
|
||||
### 1. Collect basic info
|
||||
```
|
||||
ชื่อลูกค้า: [name]
|
||||
URL เว็บไซต์: [url]
|
||||
ประเภท: astro / wordpress
|
||||
```
|
||||
|
||||
### 2. Browse website → extract contact info
|
||||
|
||||
Fetch the website and extract contact information:
|
||||
- **Phone**: look for tel:, phone patterns (0xx-xxx-xxxx)
|
||||
- **Email**: look for mailto:, email patterns
|
||||
- **LINE**: look for LINE ID (@xxx, line.me links)
|
||||
- **Facebook**: look for facebook.com/ links
|
||||
- **Address**: look for address patterns, Google Maps links
|
||||
- **Website URL**: the main URL
|
||||
|
||||
Save extracted contact to clients-config.json:
|
||||
|
||||
```python
|
||||
config_path = os.path.expanduser("~/vault/99_System/clients-config.json")
|
||||
config = json.load(open(config_path))
|
||||
|
||||
new_client = {
|
||||
"id": client_id,
|
||||
"display_name": display_name,
|
||||
"contact": {
|
||||
"phone": extracted_phone,
|
||||
"email": extracted_email,
|
||||
"line": extracted_line,
|
||||
"facebook_url": extracted_facebook,
|
||||
"website_url": website_url,
|
||||
"address": extracted_address,
|
||||
"cta_text": "" # generate below
|
||||
},
|
||||
"website": {
|
||||
"type": website_type,
|
||||
"url": website_url,
|
||||
"categories": [],
|
||||
"categories_cached_at": None
|
||||
},
|
||||
"social": { "facebook": {}, "instagram": {}, "x": {} },
|
||||
"analytics": {}
|
||||
}
|
||||
|
||||
config['clients'].append(new_client)
|
||||
json.dump(config, open(config_path, 'w'), indent=2, ensure_ascii=False)
|
||||
```
|
||||
|
||||
### 3. Generate CTA text
|
||||
|
||||
Based on extracted contact, generate a default CTA:
|
||||
- If LINE: "สอบถามเพิ่มเติม ทัก LINE: @{line_id}"
|
||||
- If phone: "ติดต่อ {phone}"
|
||||
- If email: "อีเมล {email}"
|
||||
- If facebook: "Inbox Facebook: {facebook_url}"
|
||||
- Combine available channels
|
||||
|
||||
### 4. Create per-client folders
|
||||
|
||||
```bash
|
||||
mkdir -p ~/vault/30_Research/<client-id>
|
||||
mkdir -p ~/vault/30_Research/<client-id>/Archive
|
||||
mkdir -p ~/vault/60_Articles/<client-id>
|
||||
mkdir -p ~/vault/60_Articles/<client-id>/Archive
|
||||
```
|
||||
|
||||
### 5. Confirm
|
||||
|
||||
```
|
||||
✅ ลูกค้าใหม่: [display_name]
|
||||
📞 ติดต่อ: [contact summary]
|
||||
💬 CTA: [cta_text]
|
||||
📂 Folders created: 30_Research/<id>/ + 60_Articles/<id>/
|
||||
```
|
||||
|
||||
## Process
|
||||
|
||||
### Step 1: Receive the idea and determine client
|
||||
|
||||
The user provides an idea in any form:
|
||||
- A keyword (e.g., "AI marketing")
|
||||
- A concept (e.g., "ทำไมธุรกิจเล็กควรใช้ AI")
|
||||
- A question (e.g., "CMS ตัวไหนดีที่สุดสำหรับบล็อก?")
|
||||
- A rough topic (e.g., "เขียนเรื่อง WordPress vs Astro")
|
||||
|
||||
First, determine which client this article is for:
|
||||
|
||||
```
|
||||
บทความนี้สำหรับลูกค้าไหน?
|
||||
|
||||
1. [client-1] — [display_name]
|
||||
2. [client-2] — [display_name]
|
||||
...
|
||||
N. ลูกค้าใหม่ — เพิ่มข้อมูลลูกค้า
|
||||
```
|
||||
|
||||
**If new client selected**: run the Client Onboarding flow (see below).
|
||||
|
||||
Extract the core topic and ask:
|
||||
|
||||
```
|
||||
ได้ไอเดีย: [topic]
|
||||
|
||||
ขอถามเพิ่มเติมก่อนเริ่ม research:
|
||||
1. กลุ่มเป้าหมายบทความนี้คือใคร?
|
||||
2. อยากให้บทความนี้ตอบคำถามอะไร?
|
||||
3. มีมุมมองเฉพาะที่อยากเน้นไหม?
|
||||
4. โทน: ทางการ / casual / technical / beginner-friendly?
|
||||
```
|
||||
|
||||
### Step 2: Vault Research (existing knowledge)
|
||||
|
||||
Search the vault for related content. Search BOTH client-specific and general:
|
||||
|
||||
1. **Search 30_Research/<client-id>/** for client's research notes
|
||||
2. **Search 30_Research/general/** for general research notes
|
||||
3. **Search 30_Research/** for any other relevant research
|
||||
4. **Search 60_Articles/<client-id>/** for existing articles (avoid duplication)
|
||||
|
||||
Present findings:
|
||||
|
||||
```
|
||||
📚 ข้อมูลที่มีอยู่แล้วใน vault:
|
||||
|
||||
Research:
|
||||
- [[Related Research 1]] — 30_Research/...
|
||||
- [[Related Research 2]] — 30_Research/...
|
||||
|
||||
Wiki Concepts:
|
||||
- [[Concept 1]] — brief summary
|
||||
- [[Concept 2]] — brief summary
|
||||
|
||||
บทความที่มีแล้ว (หลีกเลี่ยงซ้ำ):
|
||||
- [[Existing Article]] — เขียนเมื่อ YYYY-MM-DD
|
||||
|
||||
ข้อมูลที่ขาด: [what's missing that we need to research]
|
||||
```
|
||||
|
||||
### Step 3: Internet Research
|
||||
|
||||
Search the internet for current information:
|
||||
|
||||
1. **Web search** the topic in Thai and English
|
||||
2. **Find statistics** — recent data, studies, surveys
|
||||
3. **Find examples** — case studies, real-world applications
|
||||
4. **Find expert opinions** — quotes, interviews, articles
|
||||
5. **SERP analysis** — what currently ranks for this topic
|
||||
6. **Trends** — is this topic trending? Any recent developments?
|
||||
|
||||
Organize findings by category:
|
||||
|
||||
```
|
||||
🌐 Internet Research Results:
|
||||
|
||||
สถิติ/ข้อมูล:
|
||||
- [stat 1] — source: [url]
|
||||
- [stat 2] — source: [url]
|
||||
|
||||
ตัวอย่าง/Case Studies:
|
||||
- [example 1]
|
||||
- [example 2]
|
||||
|
||||
มุมมองผู้เชี่ยวชาญ:
|
||||
- [expert quote] — [who], [source]
|
||||
|
||||
SERP Analysis (Top 5 สำหรับ "[keyword]"):
|
||||
1. [title] — [url] — ครอบคลุม: [what they cover]
|
||||
2. ...
|
||||
|
||||
สิ่งที่ขาดในบทความที่มีอยู่: [content gap opportunity]
|
||||
```
|
||||
|
||||
### Step 4: Present and iterate on angles
|
||||
|
||||
Combine vault + internet research, present to user:
|
||||
|
||||
```
|
||||
📊 สรุปข้อมูลทั้งหมด:
|
||||
|
||||
หัวข้อ: [topic]
|
||||
กลุ่มเป้าหมาย: [audience]
|
||||
|
||||
มุมที่น่าเขียน:
|
||||
1. [Angle A] — [why this angle works]
|
||||
2. [Angle B] — [why this angle works]
|
||||
3. [Angle C] — [why this angle works]
|
||||
|
||||
เลือกมุมที่ต้องการ (หรือเสนอเพิ่ม):
|
||||
```
|
||||
|
||||
After user selects, ask:
|
||||
|
||||
```
|
||||
มุมที่เลือก: [selected angle]
|
||||
|
||||
ต้องการเพิ่มประเด็นไหนอีกไหม?
|
||||
- ประเด็นที่อยากเน้นเป็นพิเศษ?
|
||||
- ข้อมูลที่อยากให้มีในบทความ?
|
||||
- คำถามที่อยากให้บทความตอบ?
|
||||
```
|
||||
|
||||
### Step 5: Deep research on selected angle
|
||||
|
||||
Based on the selected angle, do targeted research:
|
||||
|
||||
1. Search for more specific information on the angle
|
||||
2. Find supporting data for each key point
|
||||
3. Look for counter-arguments to address
|
||||
4. Find visual素材 opportunities (charts, diagrams, screenshots)
|
||||
|
||||
### Step 6: Iterate until complete
|
||||
|
||||
Loop Steps 4-5 until:
|
||||
- User says "พอแล้ว" / "complete" / "ready"
|
||||
- No more angles to explore
|
||||
- Sufficient data collected
|
||||
|
||||
Present final summary:
|
||||
|
||||
```
|
||||
📋 Final Research Summary:
|
||||
|
||||
หัวข้อ: [topic]
|
||||
มุม: [angle]
|
||||
กลุ่มเป้าหมาย: [audience]
|
||||
โทน: [tone]
|
||||
|
||||
ประเด็นที่จะเขียน:
|
||||
1. [Point 1] — supported by [data]
|
||||
2. [Point 2] — supported by [data]
|
||||
3. [Point 3] — supported by [data]
|
||||
|
||||
ข้อมูลสนับสนุน:
|
||||
- [stat/data 1] — source: [url]
|
||||
- [stat/data 2] — source: [url]
|
||||
|
||||
Outline ที่แนะนำ:
|
||||
H1: [Title]
|
||||
H2: [Section 1]
|
||||
H2: [Section 2]
|
||||
H2: [Section 3]
|
||||
H2: FAQ
|
||||
|
||||
สร้าง brief.md เลยไหม?
|
||||
```
|
||||
|
||||
### Step 7: Create brief.md
|
||||
|
||||
Create the article brief at:
|
||||
|
||||
```
|
||||
~/vault/60_Articles/<client-id>/YYYY-MM-DD-<slug>/brief.md
|
||||
```
|
||||
|
||||
```markdown
|
||||
---
|
||||
type: brief
|
||||
status: ready
|
||||
created: YYYY-MM-DD
|
||||
related_client: "<client-id>"
|
||||
source_research: ""
|
||||
source_path: ""
|
||||
related_vault_notes:
|
||||
- "[[Note 1]]"
|
||||
- "[[Note 2]]"
|
||||
---
|
||||
|
||||
# Article Brief: [Topic]
|
||||
|
||||
## Source
|
||||
- Research: Internet + Vault synthesis
|
||||
- Related notes: [[Note 1]], [[Note 2]]
|
||||
|
||||
## Target
|
||||
- Audience: [who]
|
||||
- Tone: [formal/casual/technical]
|
||||
- Length: [target word count]
|
||||
- SEO Keyword: [primary keyword]
|
||||
|
||||
## Key Angles
|
||||
1. [Angle 1]
|
||||
2. [Angle 2]
|
||||
3. [Angle 3]
|
||||
|
||||
## Outline
|
||||
### H1: [Proposed title]
|
||||
### H2: [Section 1]
|
||||
- [Key point] — supported by: [data source]
|
||||
### H2: [Section 2]
|
||||
- [Key point] — supported by: [data source]
|
||||
### H2: [Section 3]
|
||||
- [Key point] — supported by: [data source]
|
||||
### H2: FAQ
|
||||
- [Question 1]
|
||||
- [Question 2]
|
||||
|
||||
## Research Data
|
||||
### Statistics
|
||||
- [stat 1] — source: [url]
|
||||
- [stat 2] — source: [url]
|
||||
|
||||
### Expert Quotes
|
||||
- "[quote]" — [who], [source]
|
||||
|
||||
### Examples/Case Studies
|
||||
- [example 1]
|
||||
|
||||
### SERP Gaps
|
||||
- [what competitors miss that we can cover]
|
||||
|
||||
## Related Content
|
||||
- [[Related Note 1]]
|
||||
- [[Related Note 2]]
|
||||
|
||||
## Content Requirements
|
||||
- Featured image: [description]
|
||||
- Inline images: [list with descriptions]
|
||||
- FAQ section: yes
|
||||
- Schema markup: Article/BlogPosting + FAQPage
|
||||
```
|
||||
|
||||
### Step 8: Confirm
|
||||
|
||||
```
|
||||
✅ Brief created: ~/vault/60_Articles/YYYY-MM-DD-slug/brief.md
|
||||
✅ Ready for content writing
|
||||
|
||||
ถัดไป: โหลด content-writer-mm skill เพื่อเริ่มเขียนบทความ
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
- `~/vault/60_Articles/<client-id>/<date>-<slug>/brief.md` — Article brief ready for mm-content-writer
|
||||
|
||||
## Notes
|
||||
|
||||
- This skill does NOT create the article — it only prepares the brief
|
||||
- The brief is the contract between Data Stage and Content Stage
|
||||
- Internet research is mandatory — don't rely only on vault content
|
||||
- Always check `60_Articles/<client-id>/` for existing articles to avoid duplication
|
||||
- Save research notes to `30_Research/<client-id>/` if the research is substantial enough
|
||||
(use orbites-parse-knowledge skill for that)
|
||||
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
|
||||
367
skills/mm-content-orchestrator/SKILL.md
Normal file
367
skills/mm-content-orchestrator/SKILL.md
Normal file
@@ -0,0 +1,367 @@
|
||||
---
|
||||
name: mm-content-orchestrator
|
||||
argument-hint: "<command> [options]"
|
||||
description: >
|
||||
Top-level orchestrator for moreminimore content pipeline.
|
||||
Chains Data Stage (research/idea) → Content Stage (article) → Publish Article → Social Stage (posts) → Publish Social.
|
||||
Manages the full lifecycle of content creation and publishing.
|
||||
Use when: "moreminimore", "mm orchestrate", "content pipeline", "เริ่มงาน content",
|
||||
"สร้างบทความ", "เขียนและเผยแพร่", "mm".
|
||||
---
|
||||
|
||||
# mm-content-orchestrator
|
||||
|
||||
Top-level orchestrator that chains all moreminimore skills into a complete
|
||||
content pipeline: Data Stage → Content Stage → Publish Article → Social Stage → Publish Social.
|
||||
|
||||
## When This Must Trigger
|
||||
|
||||
- "moreminimore", "mm", "mm orchestrate"
|
||||
- "content pipeline", "เริ่มงาน content"
|
||||
- "สร้างบทความและเผยแพร่", "เขียนและเผยแพร่"
|
||||
- "run content workflow", "full content pipeline"
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────┐
|
||||
│ mm-content-orchestrator │
|
||||
│ │
|
||||
│ 1. Determine entry point (research or idea) │
|
||||
│ 2. Run Data Stage → produces brief.md │
|
||||
│ 3. Run Content Stage → produces article + images │
|
||||
│ 4. Run Publish Article → get published_url │
|
||||
│ 5. Run Social Stage → produces posts (with URL) │
|
||||
│ 6. Run Publish Social → publish posts + archive │
|
||||
└──────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────┐
|
||||
│ Data Stage │
|
||||
│ │
|
||||
│ Entry A: │
|
||||
│ mm-article-from │
|
||||
│ -research │
|
||||
│ (Research→Brief)│
|
||||
│ │
|
||||
│ Entry B: │
|
||||
│ mm-article-idea │
|
||||
│ -extract │
|
||||
│ (Idea→Brief) │
|
||||
└────────┬────────┘
|
||||
│ brief.md
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Content Stage │
|
||||
│ │
|
||||
│ mm-content- │
|
||||
│ writer │
|
||||
│ │
|
||||
│ 1. Write article│
|
||||
│ 2. GEO optimize │
|
||||
│ 3. Generate imgs│
|
||||
│ 4. Approve │
|
||||
└────────┬────────┘
|
||||
│ article (draft)
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Publish Stage │
|
||||
│ (Article) │
|
||||
│ │
|
||||
│ mm-publish- │
|
||||
│ content │
|
||||
│ │
|
||||
│ • Publish article│
|
||||
│ • Get published │
|
||||
│ URL │
|
||||
│ • Save to │
|
||||
│ frontmatter │
|
||||
└────────┬────────┘
|
||||
│ published_url
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Social Stage │
|
||||
│ │
|
||||
│ mm-social- │
|
||||
│ writer │
|
||||
│ │
|
||||
│ 1. Read article │
|
||||
│ + URL │
|
||||
│ 2. Create posts │
|
||||
│ (FB, X, IG) │
|
||||
│ 3. Approve │
|
||||
└────────┬────────┘
|
||||
│ posts (draft)
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Publish Stage │
|
||||
│ (Social) │
|
||||
│ │
|
||||
│ mm-publish- │
|
||||
│ content │
|
||||
│ │
|
||||
│ • Publish posts │
|
||||
│ • Archive check │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
## Quick Commands
|
||||
|
||||
### Full pipeline (from research)
|
||||
```
|
||||
mm from research
|
||||
```
|
||||
→ Scans 30_Research/ → user picks one → creates brief → writes article → publishes → creates posts → publishes posts
|
||||
|
||||
### Full pipeline (from idea)
|
||||
```
|
||||
mm idea: [topic]
|
||||
```
|
||||
→ Researches topic → creates brief → writes article → publishes → creates posts → publishes posts
|
||||
|
||||
### Content only (no publish)
|
||||
```
|
||||
mm write [slug]
|
||||
```
|
||||
→ Finds brief → writes article + images (no publish)
|
||||
|
||||
### Publish article only
|
||||
```
|
||||
mm publish article [slug]
|
||||
```
|
||||
→ Finds approved article → publishes to website → gets URL
|
||||
|
||||
### Create social posts only
|
||||
```
|
||||
mm social [slug]
|
||||
```
|
||||
→ Finds published article with URL → creates social posts
|
||||
|
||||
### Publish social only
|
||||
```
|
||||
mm publish social [slug]
|
||||
```
|
||||
→ Finds approved social posts → publishes to platforms
|
||||
|
||||
### Status check
|
||||
```
|
||||
mm status
|
||||
```
|
||||
→ Shows all articles in pipeline with their current status
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Determine command
|
||||
|
||||
Parse the user's message to determine which command:
|
||||
|
||||
| Pattern | Command |
|
||||
|---------|---------|
|
||||
| "from research", "จาก research" | full-pipeline-research |
|
||||
| "idea: [topic]", "ไอเดีย: [topic]" | full-pipeline-idea |
|
||||
| "write [slug]", "เขียน [slug]" | content-only |
|
||||
| "publish article [slug]" | publish-article |
|
||||
| "social [slug]", "เขียนโพส [slug]" | social-only |
|
||||
| "publish social [slug]" | publish-social |
|
||||
| "status", "สถานะ" | status-check |
|
||||
|
||||
If ambiguous, ask:
|
||||
|
||||
```
|
||||
ต้องการดำเนินการอะไร?
|
||||
|
||||
1. 📚 เริ่มจาก Research — เลือก research จาก vault → เขียน → publish → โพส
|
||||
2. 💡 เริ่มจากไอเดีย — ระบุหัวข้อ → research → เขียน → publish → โพส
|
||||
3. ✍️ เขียนเท่านั้น — มี brief แล้ว → เขียนบทความ + รูป
|
||||
4. 📤 Publish บทความ — บทความพร้อม → publish → ได้ URL
|
||||
5. 📱 เขียนโพส — บทความ publish แล้ว → สร้าง social posts
|
||||
6. 📤 Publish โพส — โพสพร้อม → publish ไป social
|
||||
7. 📊 ดูสถานะ — ดู pipeline ทั้งหมด
|
||||
```
|
||||
|
||||
### Step 2: Execute Data Stage (if full pipeline)
|
||||
|
||||
#### 2a. From Research (mm-article-from-research)
|
||||
|
||||
Load and follow the `mm-article-from-research` skill:
|
||||
1. Scan 30_Research/<client-id>/ + 30_Research/general/ for unused resources
|
||||
2. User selects one
|
||||
3. Summarize and ask for focus
|
||||
4. Create brief.md in 60_Articles/<client-id>/
|
||||
|
||||
#### 2b. From Idea (mm-article-idea-extract)
|
||||
|
||||
Load and follow the `mm-article-idea-extract` skill:
|
||||
1. Receive idea from user
|
||||
2. Research in vault + internet
|
||||
3. Iterate on angles
|
||||
4. Create brief.md in 60_Articles/<client-id>/
|
||||
|
||||
### Step 3: Execute Content Stage (mm-content-writer)
|
||||
|
||||
Load and follow the `mm-content-writer` skill:
|
||||
1. Read brief.md
|
||||
2. Write article with SEO + GEO
|
||||
3. Generate images (Nano Banana)
|
||||
4. Present for approval
|
||||
|
||||
**This stage produces the article only — no social posts.**
|
||||
|
||||
### Step 4: Execute Publish Article (mm-publish-content)
|
||||
|
||||
Load and follow the `mm-publish-content` skill:
|
||||
1. Find approved articles
|
||||
2. Look up client config for credentials
|
||||
3. Publish article to website (Astro/WordPress)
|
||||
4. **Capture published_url** — save to frontmatter
|
||||
|
||||
### Step 5: Execute Social Stage (mm-social-writer)
|
||||
|
||||
Load and follow the `mm-social-writer` skill:
|
||||
1. Read published article with published_url
|
||||
2. Create social posts (Facebook, X, IG) with article URL embedded
|
||||
3. Generate social images
|
||||
4. Present for approval
|
||||
|
||||
### Step 6: Execute Publish Social (mm-publish-content)
|
||||
|
||||
Load and follow the `mm-publish-content` skill again:
|
||||
1. Find approved social posts
|
||||
2. Publish to each platform
|
||||
3. Run archive check
|
||||
|
||||
### Step 7: Summary
|
||||
|
||||
```
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
Pipeline Complete
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
📚 Data Stage:
|
||||
Source: [research title / idea]
|
||||
Brief: 60_Articles/<client-id>/slug/brief.md
|
||||
|
||||
✍️ Content Stage:
|
||||
Article: [title] — [word count] words
|
||||
Images: [N] generated
|
||||
|
||||
📤 Publish — Article:
|
||||
Website: [Astro/WordPress] ✅
|
||||
URL: [published_url]
|
||||
|
||||
📱 Social Stage:
|
||||
Posts: Facebook ✅ | X ✅ | IG ✅
|
||||
|
||||
📤 Publish — Social:
|
||||
Facebook: ✅
|
||||
X/Twitter: ✅
|
||||
Instagram: ✅
|
||||
|
||||
📂 All files: ~/vault/60_Articles/<client-id>/<slug>/
|
||||
```
|
||||
|
||||
## Status Check (mm status)
|
||||
|
||||
When user asks for status, scan 60_Articles/ per-client:
|
||||
|
||||
```python
|
||||
import os, re
|
||||
|
||||
vault = os.path.expanduser("~/vault/60_Articles")
|
||||
articles = []
|
||||
|
||||
for client_dir in os.listdir(vault):
|
||||
client_path = os.path.join(vault, client_dir)
|
||||
if not os.path.isdir(client_path) or client_dir == 'Archive':
|
||||
continue
|
||||
|
||||
for entry in sorted(os.listdir(client_path), reverse=True):
|
||||
if entry == 'Archive':
|
||||
continue
|
||||
article_path = os.path.join(client_path, entry, 'article.md')
|
||||
if not os.path.exists(article_path):
|
||||
continue
|
||||
content = open(article_path).read()[:500]
|
||||
|
||||
status_match = re.search(r'^status:\s*(.+)$', content, re.MULTILINE)
|
||||
title_match = re.search(r'^title:\s*["\']?(.+?)["\']?\s*$', content, re.MULTILINE)
|
||||
published_match = re.search(r'^published_to:\s*\[(.+?)\]', content, re.MULTILINE)
|
||||
url_match = re.search(r'^published_url:\s*["\']?(.+?)["\']?\s*$', content, re.MULTILINE)
|
||||
|
||||
status = status_match.group(1).strip() if status_match else 'unknown'
|
||||
title = title_match.group(1) if title_match else entry
|
||||
published = published_match.group(1) if published_match else ''
|
||||
url = url_match.group(1) if url_match else ''
|
||||
|
||||
# Check for social posts
|
||||
has_posts = any(os.path.exists(os.path.join(client_path, entry, f'post-{p}.md'))
|
||||
for p in ['facebook', 'x', 'ig'])
|
||||
|
||||
articles.append({
|
||||
'client': client_dir,
|
||||
'slug': entry,
|
||||
'title': title,
|
||||
'status': status,
|
||||
'published_to': published,
|
||||
'url': url,
|
||||
'has_posts': has_posts
|
||||
})
|
||||
```
|
||||
|
||||
Present as a table:
|
||||
|
||||
```
|
||||
📊 Content Pipeline Status
|
||||
|
||||
| Client | Title | Status | URL | Posts |
|
||||
|--------|-------|--------|-----|-------|
|
||||
| moreminimore | AI Marketing Guide | published | ✅ | ✅ |
|
||||
| moreminimore | SEO Tips 2026 | approved | — | — |
|
||||
| client-b | CMS Comparison | draft | — | — |
|
||||
```
|
||||
|
||||
## Config
|
||||
|
||||
Client credentials are stored in `~/vault/99_System/clients-config.json`.
|
||||
|
||||
See `mm-publish-content` skill for the full config schema.
|
||||
|
||||
If config is missing or empty:
|
||||
|
||||
```
|
||||
⚠️ ยังไม่ได้ตั้งค่า clients-config.json
|
||||
|
||||
ต้องการตั้งค่าตอนนี้ไหม?
|
||||
1. ตั้งค่าเลย — เพิ่ม client แรก
|
||||
2. ข้าม — publish ทีหลัง
|
||||
3. ดูตัวอย่าง — ดู format ของ config
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
If any stage fails:
|
||||
1. Report the error clearly
|
||||
2. Save progress (brief.md and partial outputs are preserved)
|
||||
3. Offer to resume from the failed step
|
||||
|
||||
```
|
||||
❌ [Stage] failed: [error]
|
||||
|
||||
Progress saved:
|
||||
✅ [completed steps]
|
||||
❌ [failed step]
|
||||
|
||||
ต้องการ:
|
||||
1. ลองใหม่จาก [failed step]
|
||||
2. ข้ามไป step ถัดไป
|
||||
3. หยุดและแก้ไขปัญหา
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **User approval is required** before publishing article and before publishing social posts
|
||||
- **Config is required** before publishing (Publish Stage)
|
||||
- Each stage produces files in `~/vault/60_Articles/<client-id>/<slug>/`
|
||||
- Research sources are in `~/vault/30_Research/<client-id>/` or `30_Research/general/`
|
||||
- Published content moves to `~/vault/60_Articles/<client-id>/Archive/` when ALL parts are published
|
||||
- The pipeline is resumable — each stage checks for existing outputs
|
||||
- Frontmatter is the source of truth for status tracking
|
||||
385
skills/mm-content-writer/SKILL.md
Normal file
385
skills/mm-content-writer/SKILL.md
Normal file
@@ -0,0 +1,385 @@
|
||||
---
|
||||
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
|
||||
112
skills/mm-content-writer/references/image-generation-pitfalls.md
Normal file
112
skills/mm-content-writer/references/image-generation-pitfalls.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# Image Generation Pitfalls
|
||||
|
||||
Real session learnings from batch article + image generation on moreminimore.com.
|
||||
|
||||
## The 60-Image Problem (Session: June 30, 2026)
|
||||
|
||||
14 articles needed 1 featured + ~3 inline images each = ~56 images total.
|
||||
- **46 images saved to disk** → 14 featured + 32 inline attachments
|
||||
- **~20 images lost** → generated successfully by FAL.ai but subagents timed out before saving URLs
|
||||
|
||||
## Root Cause
|
||||
|
||||
### 1. Subagent timeout on image_generate
|
||||
|
||||
Each `image_generate` call takes 15-30 seconds. A subagent trying to generate 3+ images hits the 600s timeout limit.
|
||||
|
||||
**Symptoms:**
|
||||
- Subagent returns with partial results: "I generated 2/3 images but the subagent timed out"
|
||||
- Some URLs are returned in the subagent's summary but not downloaded
|
||||
- No way to replay the lost URLs — FAL.ai doesn't keep a history
|
||||
|
||||
**Fix:** Generate ALL images from the main agent, never delegate. Each call is a single tool invocation; batch by running up to 3-4 sequential calls per turn.
|
||||
|
||||
### 1b. Lost URLs CAN be recovered via FAL API history (CRITICAL)
|
||||
|
||||
If a subagent times out after calling `image_generate`, the returned image URLs **can be recovered** — FAL.ai exposes a REST endpoint for request history with image URLs. Here's how:
|
||||
|
||||
**Recovery technique (proven in session June 30–31, 2026 — 62 URLs recovered):**
|
||||
|
||||
```bash
|
||||
# 1. List ALL requests by endpoint (latest first):
|
||||
curl --request GET \
|
||||
--url 'https://api.fal.ai/v1/models/requests/by-endpoint?limit=100&sort_by=ended_at' \
|
||||
--header 'Authorization: Key <FAL_KEY>'
|
||||
|
||||
# 2. Get detailed info INCLUDING image URLs (CRITICAL: expand=payloads):
|
||||
curl --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>'
|
||||
```
|
||||
|
||||
The `expand=payloads` parameter is critical — without it, the response only contains metadata (request_id, model_id, status, timestamps, token count). With `expand=payloads`, each request includes `request.payload.json_output` which contains output fields including `images[].url`.
|
||||
|
||||
**Download workflow after recovery:**
|
||||
```bash
|
||||
curl -sL "<image_url>" -o attachments/<name>.png
|
||||
file attachments/<name>.png # verify it's a real image
|
||||
ls -la attachments/<name>.png # verify non-empty
|
||||
```
|
||||
|
||||
Full example from a real recovery session — see `moreminimore-orchestrator/references/fal-api-recovery.md`.
|
||||
|
||||
**When NOT to use this:**
|
||||
- If the FAL key has changed since the images were generated (history is per-key)
|
||||
- If the requests are older than FAL's retention window (unknown; 30+ days confirmed working)
|
||||
|
||||
**Net result proven in real session:** 62 previously-generated-but-lost image URLs recovered and downloaded from FAL history, avoiding re-generation at ~62 FAL.ai credits.
|
||||
|
||||
### 2. Placeholder vs real image mismatch
|
||||
|
||||
Initial articles had `<!-- PLACEHOLDER: image — description -->` comments.
|
||||
These were written by subagents that don't have access to image generation.
|
||||
|
||||
**Fix:** Every brief must specify exact image filenames. Every article.md must reference real paths. After all images are saved, grep for `PLACEHOLDER` to catch any missed replacements.
|
||||
|
||||
### 3. FAL.ai balance exhaustion (intermittent)
|
||||
|
||||
Balance runs out mid-session after ~30-40 images. Subsequent calls return:
|
||||
```
|
||||
User is locked. Reason: Exhausted balance. Top up your balance at fal.ai/dashboard/billing.
|
||||
```
|
||||
|
||||
**Detection:** The error is `FalClientHTTPError` with `Exhausted balance`. This is NOT a transient failure — retrying won't help. Stop immediately and tell the user.
|
||||
|
||||
**Recovery:** After top-up, resume from where you left off. Don't regenerate already-saved images. Use `find . -name "*.png"` to enumerate existing images per article, then only generate missing ones.
|
||||
|
||||
### 4. Inline images for batch mode — separate pass
|
||||
|
||||
Batch mode is optimized for speed. The skill says "featured images only" for batch, but:
|
||||
- User will likely want inline images later
|
||||
- When they ask, it's a separate pass (same process, different priority)
|
||||
- Each inline image pass costs 3× article count in FAL.ai credits
|
||||
|
||||
**Warn the user before starting an inline-image pass on a batch:** "This will generate ~42 images and consume significant FAL.ai credits. Are you sure?"
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
After any image generation session, run this:
|
||||
|
||||
```bash
|
||||
cd ~/vault/60_Articles
|
||||
|
||||
# 1. Count all images
|
||||
find . -name "*.png" | wc -l
|
||||
|
||||
# 2. Find any remaining placeholders
|
||||
grep -rn "PLACEHOLDER\|TODO_IMAGE\|<!-- IMAGE" . --include="*.md" | grep -v "brief.md"
|
||||
|
||||
# 3. Count per article
|
||||
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
|
||||
```
|
||||
|
||||
## Cost Reference
|
||||
|
||||
- FAL Nano Banana Pro (Gemini 3 Pro Image): ~1 credit per image
|
||||
- 14 articles batch: ~14 credits (featured only)
|
||||
- 14 articles + inline: ~56 credits
|
||||
- 14 articles + all replacements: ~80+ credits (if regenerating replacements)
|
||||
635
skills/mm-publish-content/SKILL.md
Normal file
635
skills/mm-publish-content/SKILL.md
Normal file
@@ -0,0 +1,635 @@
|
||||
---
|
||||
name: mm-publish-content
|
||||
description: >
|
||||
Publish Stage: Publishes approved articles and social posts to multiple platforms.
|
||||
Supports Astro (git push), WordPress (REST API), Facebook (Page API),
|
||||
X/Twitter (xurl CLI), and Instagram (Graph API).
|
||||
Reads clients-config.json for credentials. Marks published status in frontmatter.
|
||||
Use when: "publish article", "publish บทความ", "ส่งบทความ", "ลงบทความ",
|
||||
"publish to astro", "publish to wordpress", "post to social".
|
||||
---
|
||||
|
||||
# mm-publish-content
|
||||
|
||||
Publishes approved content to websites and social media platforms.
|
||||
|
||||
## When This Must Trigger
|
||||
|
||||
- "publish article", "publish บทความ", "ส่งบทความ", "ลงบทความ"
|
||||
- "publish to astro", "publish to wordpress"
|
||||
- "post to facebook", "post to x", "post to instagram"
|
||||
- "ลง social", "publish social"
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Article must have `status: approved` in frontmatter
|
||||
- Social posts must exist (created by mm-content-writer)
|
||||
- Credentials must be configured in `~/vault/99_System/clients-config.json`
|
||||
|
||||
## Process
|
||||
|
||||
### Step 1: Find content to publish
|
||||
|
||||
```bash
|
||||
# List articles ready to publish
|
||||
# Look for articles with status: approved in 60_Articles/<client-id>/
|
||||
VAULT="$HOME/vault"
|
||||
```
|
||||
|
||||
Search for articles with `status: approved` in per-client folders:
|
||||
|
||||
```python
|
||||
import os, re
|
||||
|
||||
vault = os.path.expanduser("~/vault/60_Articles")
|
||||
ready = []
|
||||
|
||||
for root, dirs, files in os.walk(vault):
|
||||
for f in files:
|
||||
if f != 'article.md':
|
||||
continue
|
||||
path = os.path.join(root, f)
|
||||
content = open(path).read()
|
||||
if 'status: approved' in content[:500]:
|
||||
# Extract title and slug
|
||||
title_match = re.search(r'^title:\s*["\']?(.+?)["\']?\s*$', content, re.MULTILINE)
|
||||
title = title_match.group(1) if title_match else f
|
||||
slug = os.path.basename(root)
|
||||
ready.append({'path': path, 'title': title, 'slug': slug, 'dir': root})
|
||||
```
|
||||
|
||||
If no approved articles found:
|
||||
```
|
||||
ไม่พบบทความที่พร้อม publish
|
||||
บทความต้องมี status: approved ก่อน
|
||||
ใช้ mm-content-writer skill เพื่อเขียนและอนุมัติบทความ
|
||||
```
|
||||
|
||||
If multiple found, ask which one to publish (or publish all).
|
||||
|
||||
### Step 2: Determine publish targets
|
||||
|
||||
Read the article's frontmatter:
|
||||
- `related_client` — look up in clients-config.json
|
||||
- `published_to` — already published platforms (skip these)
|
||||
|
||||
If `related_client` is set, load client config:
|
||||
|
||||
```python
|
||||
import json
|
||||
|
||||
config_path = os.path.expanduser("~/vault/99_System/clients-config.json")
|
||||
config = json.load(open(config_path))
|
||||
|
||||
client = None
|
||||
for c in config.get('clients', []):
|
||||
if c['id'] == related_client:
|
||||
client = c
|
||||
break
|
||||
```
|
||||
|
||||
If no `related_client` or client not found:
|
||||
|
||||
```
|
||||
บทความนี้ยังไม่ได้ระบุ client
|
||||
ต้องการ publish ไปที่ไหน?
|
||||
|
||||
1. Astro website (specify repo)
|
||||
2. WordPress website (specify URL)
|
||||
3. Social media only (Facebook, X, IG)
|
||||
4. ระบุ client จาก clients-config.json
|
||||
```
|
||||
|
||||
If client is found, show available platforms:
|
||||
|
||||
```
|
||||
📋 Client: [display_name]
|
||||
|
||||
🌐 Website:
|
||||
- [type]: [url/repo] ✅
|
||||
|
||||
📱 Social:
|
||||
- Facebook: [page_id] ✅/❌
|
||||
- Instagram: [user_id] ✅/❌
|
||||
- X: [username] ✅/❌
|
||||
|
||||
เผยแพร่ไปช่องทางไหนบ้าง? (เลือกหมายเลข หรือ "all"):
|
||||
1. Website ([type])
|
||||
2. Facebook
|
||||
3. Instagram
|
||||
4. X/Twitter
|
||||
5. All available
|
||||
```
|
||||
|
||||
### Step 3: Publish to Website
|
||||
|
||||
#### 3a. Astro (git push)
|
||||
|
||||
If website type is `astro`:
|
||||
|
||||
```bash
|
||||
# 1. Clone or pull the repo
|
||||
REPO_URL="<repo from config>"
|
||||
BRANCH="<branch from config, default main>"
|
||||
ARTICLE_DIR="<path to article>"
|
||||
|
||||
# 2. Convert article.md to Astro blog format
|
||||
# - Move to src/content/blog/<slug>/index.md
|
||||
# - Convert frontmatter to Astro format
|
||||
# - Copy images to same directory
|
||||
# - Update any wikilinks to regular links
|
||||
|
||||
# 3. Git commit and push
|
||||
git add .
|
||||
git commit -m "publish: <article title>"
|
||||
git push origin $BRANCH
|
||||
```
|
||||
|
||||
Conversion rules for Astro:
|
||||
- Keep `title`, `description`, `pubDate`, `slug` in frontmatter
|
||||
- Add `heroImage` pointing to featured image
|
||||
- Remove vault-specific fields (`type`, `status`, `related_client`)
|
||||
- Convert `published_to` to include `astro`
|
||||
- Copy images to the article directory
|
||||
|
||||
#### Capture published_url
|
||||
|
||||
After successful website publish, capture the URL:
|
||||
|
||||
**For Astro**: Construct from site config + slug
|
||||
```
|
||||
PUBLISHED_URL="<site_url>/blog/<slug>"
|
||||
```
|
||||
|
||||
**For WordPress**: Get from API response
|
||||
```
|
||||
PUBLISHED_URL="<response.link>"
|
||||
```
|
||||
|
||||
#### Save published_url to frontmatter
|
||||
|
||||
Update the article's frontmatter with the published URL:
|
||||
|
||||
```python
|
||||
import re
|
||||
|
||||
article_path = "<path to article.md>"
|
||||
content = open(article_path).read()
|
||||
|
||||
# Add published_url after status: published
|
||||
content = content.replace(
|
||||
"status: published",
|
||||
f"status: published\npublished_url: \"{published_url}\""
|
||||
)
|
||||
|
||||
open(article_path, 'w').write(content)
|
||||
```
|
||||
|
||||
Also update `published_to` to include the website platform:
|
||||
```python
|
||||
# Add to published_to array
|
||||
if "published_to: []" in content:
|
||||
content = content.replace("published_to: []", f"published_to: [astro]")
|
||||
```
|
||||
|
||||
Confirm to user:
|
||||
```
|
||||
✅ บทความ published:
|
||||
URL: [published_url]
|
||||
Frontmatter updated: published_url, published_to
|
||||
```
|
||||
|
||||
#### Update existing social posts with URL
|
||||
|
||||
If social posts already exist (from a previous run), update them with the published_url:
|
||||
|
||||
```python
|
||||
import os, re
|
||||
|
||||
article_dir = "<path to article dir>"
|
||||
published_url = "<url>"
|
||||
|
||||
for platform in ['facebook', 'x', 'ig']:
|
||||
post_path = os.path.join(article_dir, f"post-{platform}.md")
|
||||
if not os.path.exists(post_path):
|
||||
continue
|
||||
|
||||
content = open(post_path).read()
|
||||
|
||||
# Add published_url to frontmatter
|
||||
if 'published_url' not in content:
|
||||
content = re.sub(
|
||||
r'^(related_article:.*)$',
|
||||
f'\\1\npublished_url: "{published_url}"',
|
||||
content,
|
||||
flags=re.MULTILINE
|
||||
)
|
||||
else:
|
||||
content = re.sub(
|
||||
r'^(published_url:\s*)["\']?.*?["\']?\s*$',
|
||||
f'\\1"{published_url}"',
|
||||
content,
|
||||
flags=re.MULTILINE
|
||||
)
|
||||
|
||||
# Replace URL placeholders in body
|
||||
content = content.replace('[ARTICLE_URL]', published_url)
|
||||
content = content.replace('[article_url]', published_url)
|
||||
|
||||
open(post_path, 'w').write(content)
|
||||
```
|
||||
|
||||
Or use the helper script:
|
||||
```bash
|
||||
python3 ~/Gitea/moreminimore-service-system/scripts/mm_update_url.py \
|
||||
--dir ~/vault/60_Articles/<client-id>/<slug> \
|
||||
--url [published_url]
|
||||
```
|
||||
| `meta_description` | `description` | First 160 chars of description or article |
|
||||
| `created` | `pubDate` | Use source date, NOT today |
|
||||
| — | `category` | Look up from client config or ask user. Must match one of client's blog categories exactly |
|
||||
| — | `heroImage` | `/images/blog/<slug>-<name>.png` — the ASTRO path, not the vault path |
|
||||
| — | `draft: false` | Always add for publication |
|
||||
| `type`, `status`, `related_client` | (omit) | Strip vault-internal fields |
|
||||
| `published_to` | (omit from frontmatter) | Track separately |
|
||||
|
||||
**Image relocation:**
|
||||
- Vault featured: `60_Articles/<slug>/images/featured.png`
|
||||
→ Astro: `public/images/blog/<slug>-featured.png`
|
||||
- Vault inline: `60_Articles/<slug>/attachments/<name>.png`
|
||||
→ Astro: `public/images/blog/<slug>-<name>.png`
|
||||
- Every image reference in markdown: must rewrite vault path to Astro path
|
||||
|
||||
**Example output:**
|
||||
```markdown
|
||||
---
|
||||
title: "Fintech มองไม่เห็นโดย AI Agents — แล้วจะเปลี่ยนยังไง?"
|
||||
description: "ธุรกิจการเงินพร้อมรับ AI Agents หรือยัง? วิเคราะห์ 3 แนวทางสร้าง visibility"
|
||||
pubDate: 2026-06-26
|
||||
category: "SEO"
|
||||
heroImage: "/images/blog/2026-06-26-fintech-invisible-to-ai-agents-featured.png"
|
||||
draft: false
|
||||
---
|
||||
|
||||
# Fintech มองไม่เห็นโดย AI Agents — แล้วจะเปลี่ยนยังไง?
|
||||
|
||||

|
||||
```
|
||||
|
||||
**Path conventions:**
|
||||
- Always use absolute paths from root: `/images/blog/<slug>-<name>.png`
|
||||
- Never use relative paths (`images/...` or `../images/...`)
|
||||
- Prefix inline image names with the article slug to avoid collisions across articles
|
||||
|
||||
**Post-conversion verification (MANDATORY):**
|
||||
```bash
|
||||
# 1. Verify all Astro blog files exist
|
||||
cd ~/Gitea/moreminimore-astroreal
|
||||
ls src/content/blog/2026-*.md | wc -l
|
||||
|
||||
# 2. Verify all image references exist in public/
|
||||
grep -oP '/images/blog/[^)\"\\s]+' src/content/blog/2026-*.md | sort -u | while IFS= read -r img; do
|
||||
[ -f "public$img" ] || echo "MISSING: public$img"
|
||||
done
|
||||
|
||||
# 3. Build check
|
||||
npm run build 2>&1 | tail -5
|
||||
# Must show "[N] page(s) built" without errors
|
||||
```
|
||||
|
||||
**Git commit + push:**
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "feat: publish [article title]"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
**UPDATED `clients-config.json`:**
|
||||
After publishing, update the client entry with:
|
||||
- `website.categories` — the full list of categories used (for content planning)
|
||||
- `website.last_published` — ISO date of latest publish batch
|
||||
- Keep `website.repo` pointing to the Astro repo path
|
||||
|
||||
#### 3b. WordPress (REST API)
|
||||
|
||||
If website type is `wordpress`:
|
||||
|
||||
```bash
|
||||
# WordPress REST API
|
||||
WP_URL="<url from config>"
|
||||
WP_USER="<user from config>"
|
||||
WP_APP_PASSWORD="<password from config>"
|
||||
|
||||
# Create post
|
||||
curl -X POST "$WP_URL/wp-json/wp/v2/posts" \
|
||||
-u "$WP_USER:$WP_APP_PASSWORD" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"title": "<title>",
|
||||
"content": "<html content>",
|
||||
"status": "draft",
|
||||
"slug": "<slug>",
|
||||
"categories": [<category_ids>],
|
||||
"featured_media": <media_id>
|
||||
}'
|
||||
```
|
||||
|
||||
Steps:
|
||||
1. Convert markdown to WordPress HTML (handle headings, lists, images)
|
||||
2. Upload featured image to WordPress Media Library → get media_id
|
||||
3. Upload inline images → get URLs
|
||||
4. Create post with content + featured_media
|
||||
5. Set status to "draft" first (user can review in WP before publishing)
|
||||
|
||||
WordPress image upload:
|
||||
```bash
|
||||
curl -X POST "$WP_URL/wp-json/wp/v2/media" \
|
||||
-u "$WP_USER:$WP_APP_PASSWORD" \
|
||||
-H "Content-Disposition: attachment; filename=\"featured.png\"" \
|
||||
-H "Content-Type: image/png" \
|
||||
--data-binary @featured.png
|
||||
```
|
||||
|
||||
### Step 4: Publish to Social Media
|
||||
|
||||
#### 4a. Facebook Page
|
||||
|
||||
Requires: `page_id` and `page_token` from client config.
|
||||
|
||||
```bash
|
||||
# Post to Facebook Page
|
||||
PAGE_ID="<from config>"
|
||||
PAGE_TOKEN="<from config>"
|
||||
|
||||
# Text post
|
||||
curl -X POST "https://graph.facebook.com/v21.0/$PAGE_ID/feed" \
|
||||
-d "message=<post content>" \
|
||||
-d "access_token=$PAGE_TOKEN"
|
||||
|
||||
# Post with image
|
||||
# 1. Upload photo first
|
||||
curl -X POST "https://graph.facebook.com/v21.0/$PAGE_ID/photos" \
|
||||
-F "source=@image.png" \
|
||||
-F "message=<caption>" \
|
||||
-F "access_token=$PAGE_TOKEN"
|
||||
```
|
||||
|
||||
Read `post-facebook.md` from the article directory.
|
||||
Post content = body of post-facebook.md.
|
||||
|
||||
#### 4b. X/Twitter (xurl CLI)
|
||||
|
||||
Requires: xurl configured with the client's app.
|
||||
|
||||
```bash
|
||||
# Check xurl auth
|
||||
xurl auth status
|
||||
|
||||
# Post single tweet
|
||||
xurl post "<tweet content>"
|
||||
|
||||
# Post with image
|
||||
xurl media upload images/featured.png
|
||||
xurl post "<tweet content>" --media-id <MEDIA_ID>
|
||||
|
||||
# Post thread (if post-x.md is a thread)
|
||||
xurl post "<tweet 1>"
|
||||
# Get tweet ID from response
|
||||
xurl reply <TWEET_ID> "<tweet 2>"
|
||||
xurl reply <TWEET_ID_2> "<tweet 3>"
|
||||
```
|
||||
|
||||
Read `post-x.md` from the article directory.
|
||||
Determine if single post or thread based on frontmatter `format` field.
|
||||
|
||||
#### 4c. Instagram (Graph API)
|
||||
|
||||
Requires: `user_id` and `token` from client config (Facebook Graph API).
|
||||
|
||||
```bash
|
||||
IG_USER_ID="<from config>"
|
||||
IG_TOKEN="<from config>"
|
||||
|
||||
# 1. Create media container
|
||||
CONTAINER_ID=$(curl -s -X POST "https://graph.facebook.com/v21.0/$IG_USER_ID/media" \
|
||||
-d "image_url=<public image url>" \
|
||||
-d "caption=<caption>" \
|
||||
-d "access_token=$IG_TOKEN" | jq -r '.id')
|
||||
|
||||
# 2. Publish container
|
||||
curl -X POST "https://graph.facebook.com/v21.0/$IG_USER_ID/media_publish" \
|
||||
-d "creation_id=$CONTAINER_ID" \
|
||||
-d "access_token=$IG_TOKEN"
|
||||
```
|
||||
|
||||
**Important**: Instagram requires images to be publicly accessible URLs.
|
||||
Options:
|
||||
1. Upload image to a public CDN first
|
||||
2. Use the WordPress media URL if already uploaded
|
||||
3. Use a temporary image hosting service
|
||||
|
||||
Read `post-ig.md` from the article directory.
|
||||
|
||||
### Step 5: Update frontmatter
|
||||
|
||||
After successful publishing, update the article's frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
...existing...
|
||||
status: published
|
||||
published_to: [astro, facebook, x, ig] # append new platforms
|
||||
published_date: YYYY-MM-DD
|
||||
---
|
||||
```
|
||||
|
||||
Also update each social post's frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
...existing...
|
||||
status: published
|
||||
published_to: [facebook]
|
||||
published_date: YYYY-MM-DD
|
||||
post_id: "<platform post ID>"
|
||||
---
|
||||
```
|
||||
|
||||
Use `patch` to update frontmatter fields.
|
||||
|
||||
### Step 6: Error handling
|
||||
|
||||
For each platform, if publishing fails:
|
||||
|
||||
```
|
||||
❌ [Platform]: [error message]
|
||||
|
||||
สาเหตุที่เป็นไปได้:
|
||||
- Token หมดอายุ → ต้อง renew
|
||||
- credentials ไม่ถูกต้อง → ตรวจสอบ clients-config.json
|
||||
- API quota หมด → รอและลองใหม่
|
||||
- [platform-specific troubleshooting]
|
||||
|
||||
ต้องการ:
|
||||
1. ข้าม platform นี้ → publish ช่องทางอื่นต่อ
|
||||
2. หยุด → แก้ไขปัญหาก่อน
|
||||
3. ลองใหม่
|
||||
```
|
||||
|
||||
### Step 7: Summary
|
||||
|
||||
**CRITICAL: Full article verification.** The user expects a per-article status report (not a summary). Generate a table showing EVERY article's status:
|
||||
|
||||
```bash
|
||||
# Per-article verification
|
||||
for f in src/content/blog/2026-*.md; do
|
||||
slug=$(basename "$f" .md)
|
||||
title=$(head -5 "$f" | grep "title:" | sed 's/title: "//' | sed 's/"$//')
|
||||
cat=$(grep "category:" "$f" | head -1 | sed 's/category: "//' | sed 's/"$//')
|
||||
hero=$(grep "heroImage:" "$f" | head -1)
|
||||
# Check if hero image exists
|
||||
if [ -n "$hero" ]; then
|
||||
img_path=$(echo "$hero" | grep -oP '/images/blog/[^"'"'"']+')
|
||||
[ -f "public$img_path" ] && img="✅" || img="❌ MISSING"
|
||||
else
|
||||
img="⚠️ no heroImage"
|
||||
fi
|
||||
echo "$slug | $title | $cat | $img"
|
||||
done
|
||||
```
|
||||
|
||||
### Step 7: Summary
|
||||
|
||||
```
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
Published: [Article Title]
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
🌐 Website:
|
||||
✅ [Astro/WordPress]: [published_url]
|
||||
|
||||
📱 Social:
|
||||
✅ Facebook: [post url or "posted"]
|
||||
✅ X/Twitter: [tweet url or "posted"]
|
||||
✅ Instagram: [posted]
|
||||
❌ [Failed platform]: [reason]
|
||||
|
||||
📋 Status updated in vault frontmatter
|
||||
📋 published_url saved: [url]
|
||||
```
|
||||
|
||||
### Step 8: Archive check (after all publishing)
|
||||
|
||||
After publishing, check if ALL content in the article directory is fully published.
|
||||
Only archive when the entire bundle is complete.
|
||||
|
||||
```python
|
||||
def can_archive(article_dir):
|
||||
"""Check if ALL content in article dir is published"""
|
||||
import os
|
||||
|
||||
# Files that must be checked
|
||||
required_files = ['article.md']
|
||||
optional_posts = ['post-facebook.md', 'post-x.md', 'post-ig.md']
|
||||
|
||||
# Article must be published
|
||||
article_path = os.path.join(article_dir, 'article.md')
|
||||
if not os.path.exists(article_path):
|
||||
return False
|
||||
content = open(article_path).read()[:500]
|
||||
if 'status: published' not in content:
|
||||
return False
|
||||
|
||||
# All existing posts must also be published
|
||||
for f in optional_posts:
|
||||
path = os.path.join(article_dir, f)
|
||||
if not os.path.exists(path):
|
||||
continue # post doesn't exist = skip
|
||||
content = open(path).read()[:500]
|
||||
if 'status: published' not in content:
|
||||
return False
|
||||
|
||||
return True
|
||||
```
|
||||
|
||||
If `can_archive()` returns True:
|
||||
|
||||
```
|
||||
✅ ทั้ง article และ posts publish ครบแล้ว — ย้ายไป Archive
|
||||
|
||||
ย้าย folder ไป Archive/? (yes/no)
|
||||
```
|
||||
|
||||
If user confirms (or auto-confirm):
|
||||
|
||||
```bash
|
||||
ARTICLE_DIR="<path>"
|
||||
CLIENT_ID="<client-id>"
|
||||
ARCHIVE_DIR="$HOME/vault/60_Articles/$CLIENT_ID/Archive"
|
||||
|
||||
mkdir -p "$ARCHIVE_DIR"
|
||||
mv "$ARTICLE_DIR" "$ARCHIVE_DIR/$(basename $ARTICLE_DIR)"
|
||||
```
|
||||
|
||||
If `can_archive()` returns False:
|
||||
|
||||
```
|
||||
⚠️ ยังไม่ครบ — ไม่ย้ายไป Archive
|
||||
|
||||
สถานะ:
|
||||
✅ article.md: published
|
||||
✅ post-facebook.md: published
|
||||
❌ post-x.md: draft (ยังไม่ publish)
|
||||
✅ post-ig.md: published
|
||||
|
||||
ต้อง publish ทุกส่วนก่อนจึงจะย้ายไป Archive ได้
|
||||
```
|
||||
|
||||
**Important**: NEVER move to Archive unless ALL existing content files
|
||||
are published. The archive represents "fully complete" work only.
|
||||
|
||||
## Config Lookup
|
||||
|
||||
Credentials are read from `~/vault/99_System/clients-config.json`.
|
||||
|
||||
Structure:
|
||||
```json
|
||||
{
|
||||
"global": {
|
||||
"google": { ... },
|
||||
"meta": { ... }
|
||||
},
|
||||
"clients": [
|
||||
{
|
||||
"id": "client-id",
|
||||
"display_name": "Client Name",
|
||||
"website": {
|
||||
"type": "wordpress|astro",
|
||||
"url": "...",
|
||||
"wp_user": "...",
|
||||
"wp_app_password": "..."
|
||||
},
|
||||
"social": {
|
||||
"facebook": { "page_id": "...", "page_token": "..." },
|
||||
"instagram": { "user_id": "...", "token": "..." },
|
||||
"x": { "username": "...", "app": "..." }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
If credentials are missing:
|
||||
1. Tell the user which credential is needed
|
||||
2. Explain how to obtain it
|
||||
3. Offer to save it to clients-config.json once provided
|
||||
|
||||
## Notes
|
||||
|
||||
- Always publish to website FIRST, then social (social posts should link to the article)
|
||||
- WordPress posts are created as "draft" by default — user reviews before publishing
|
||||
- Astro posts are pushed to git — may trigger CI/CD deployment
|
||||
- Instagram requires publicly accessible image URLs — plan accordingly
|
||||
- X/Twitter uses xurl CLI — must be installed and authenticated
|
||||
- Mark `status: published` only after ALL selected platforms succeed
|
||||
- If partial success (some platforms fail), mark only successful platforms in `published_to`
|
||||
276
skills/mm-social-writer/SKILL.md
Normal file
276
skills/mm-social-writer/SKILL.md
Normal file
@@ -0,0 +1,276 @@
|
||||
---
|
||||
name: mm-social-writer
|
||||
description: >
|
||||
Create social media posts (Facebook, X/Twitter, Instagram) from a published article.
|
||||
Requires the article's published_url. Generates platform-specific posts with
|
||||
the article link embedded. Use when: "เขียนโพส", "create social posts",
|
||||
"write posts from article", "mm-social", "สร้างโพสจากบทความ".
|
||||
---
|
||||
|
||||
# mm-social-writer
|
||||
|
||||
Creates social media posts from a published article, with the article URL embedded.
|
||||
|
||||
## When This Must Trigger
|
||||
|
||||
- "เขียนโพส", "create social posts", "write posts from article"
|
||||
- "mm-social", "สร้างโพสจากบทความ"
|
||||
- Called by moreminimore-orchestrator after article is published
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Article must be published with `published_url` in frontmatter
|
||||
- Article must have `status: published`
|
||||
|
||||
## Process
|
||||
|
||||
### Step 1: Find the published article
|
||||
|
||||
Search for published articles that DON'T have social posts yet:
|
||||
|
||||
```python
|
||||
import os, re
|
||||
|
||||
vault = os.path.expanduser("~/vault/60_Articles")
|
||||
candidates = []
|
||||
|
||||
for client_dir in os.listdir(vault):
|
||||
client_path = os.path.join(vault, client_dir)
|
||||
if not os.path.isdir(client_path) or client_dir == 'Archive':
|
||||
continue
|
||||
|
||||
for entry in os.listdir(client_path):
|
||||
if entry == 'Archive':
|
||||
continue
|
||||
article_path = os.path.join(client_path, entry, 'article.md')
|
||||
if not os.path.exists(article_path):
|
||||
continue
|
||||
|
||||
content = open(article_path).read()[:800]
|
||||
|
||||
# Must be published
|
||||
if 'status: published' not in content:
|
||||
continue
|
||||
|
||||
# Must have published_url
|
||||
url_match = re.search(r'^published_url:\s*["\']?(.+?)["\']?\s*$', content, re.MULTILINE)
|
||||
if not url_match:
|
||||
continue
|
||||
|
||||
# Check if posts already exist
|
||||
has_posts = os.path.exists(os.path.join(client_path, entry, 'post-facebook.md'))
|
||||
|
||||
title_match = re.search(r'^title:\s*["\']?(.+?)["\']?\s*$', content, re.MULTILINE)
|
||||
|
||||
candidates.append({
|
||||
'client_id': client_dir,
|
||||
'slug': entry,
|
||||
'title': title_match.group(1) if title_match else entry,
|
||||
'url': url_match.group(1),
|
||||
'dir': os.path.join(client_path, entry),
|
||||
'has_posts': has_posts
|
||||
})
|
||||
```
|
||||
|
||||
Present to user:
|
||||
|
||||
```
|
||||
บทความที่พร้อมสร้างโพส:
|
||||
|
||||
1. [title] — [client] — [url] — ยังไม่มีโพส
|
||||
2. [title] — [client] — [url] — มีโพสแล้ว
|
||||
...
|
||||
|
||||
เลือกหมายเลข:
|
||||
```
|
||||
|
||||
If user selects an article that already has posts, ask:
|
||||
- สร้างใหม่แทนโพสเดิม?
|
||||
- หรือดูโพสที่มีอยู่?
|
||||
|
||||
### Step 2: Read article content and determine platforms
|
||||
|
||||
Read the full article to understand:
|
||||
- Main topic and key points
|
||||
- Tone and style
|
||||
- Target audience
|
||||
- Key statistics or quotes to highlight
|
||||
|
||||
**Determine which platforms to create posts for:**
|
||||
|
||||
```python
|
||||
import json, os
|
||||
|
||||
config_path = os.path.expanduser("~/vault/99_System/clients-config.json")
|
||||
config = json.load(open(config_path))
|
||||
|
||||
client = None
|
||||
for c in config['clients']:
|
||||
if c['id'] == client_id:
|
||||
client = c
|
||||
break
|
||||
|
||||
# Get platforms from social_platforms field
|
||||
platforms = client.get('social_platforms', [])
|
||||
|
||||
if not platforms:
|
||||
print("⚠️ ไม่มี social_platforms ใน config — ข้ามการสร้างโพส")
|
||||
# Exit or skip
|
||||
```
|
||||
|
||||
**If `social_platforms` is empty**: skip social post creation entirely.
|
||||
|
||||
**If `social_platforms` has values**: only create posts for those platforms.
|
||||
|
||||
Present to user:
|
||||
```
|
||||
📱 Platforms ที่ใช้: [facebook, x, ig]
|
||||
(กำหนดใน clients-config.json → social_platforms)
|
||||
```
|
||||
|
||||
### Step 3: Generate social posts
|
||||
|
||||
Create platform-specific posts based on `social_platforms`:
|
||||
|
||||
**Only create posts for platforms listed in `social_platforms`.**
|
||||
Skip any platform not in the list.
|
||||
|
||||
#### 3a. Facebook Post (if "facebook" in social_platforms)
|
||||
|
||||
Use `post-formatter` logic (PAS or AIDA framework):
|
||||
- 200-300 words
|
||||
- Hook-driven opening
|
||||
- Key takeaway from article
|
||||
- **Article URL embedded** in the post
|
||||
- No CTA contact info (social posts don't use CTA)
|
||||
|
||||
```markdown
|
||||
---
|
||||
type: post
|
||||
platform: facebook
|
||||
status: draft
|
||||
created: YYYY-MM-DD
|
||||
related_article: "article.md"
|
||||
published_url: "https://..."
|
||||
---
|
||||
|
||||
[Post content with article link naturally integrated]
|
||||
```
|
||||
|
||||
#### 3b. X/Twitter Post (if "x" in social_platforms)
|
||||
|
||||
Use `hook-generator` logic for the hook:
|
||||
- Single post: 280 chars max, hook + key point + **article URL**
|
||||
- Thread: 3-5 tweets, each with a key point, last tweet has **article URL**
|
||||
|
||||
```markdown
|
||||
---
|
||||
type: post
|
||||
platform: x
|
||||
status: draft
|
||||
created: YYYY-MM-DD
|
||||
related_article: "article.md"
|
||||
published_url: "https://..."
|
||||
format: single|thread
|
||||
---
|
||||
|
||||
[Post content with article link]
|
||||
```
|
||||
|
||||
#### 3c. Instagram Post (if "instagram" in social_platforms)
|
||||
|
||||
Visual-focused post:
|
||||
- Caption: 150-200 words, hook + key takeaway
|
||||
- **Article URL in bio** (IG doesn't allow clickable links in captions)
|
||||
→ Note: "ลิงค์บทความใน bio" or use link sticker in Stories
|
||||
- Image: generate a square (1080x1080) version
|
||||
- Hashtags: 5-10 relevant hashtags
|
||||
|
||||
```markdown
|
||||
---
|
||||
type: post
|
||||
platform: instagram
|
||||
status: draft
|
||||
created: YYYY-MM-DD
|
||||
related_article: "article.md"
|
||||
published_url: "https://..."
|
||||
image: "images/ig-post.png"
|
||||
---
|
||||
|
||||
[Caption with "🔗 ลิงค์ใน bio" or similar]
|
||||
```
|
||||
|
||||
### Step 4: Generate social images
|
||||
|
||||
For each platform:
|
||||
- Facebook: use featured image (1200x630)
|
||||
- X: use featured image (1200x675)
|
||||
- Instagram: generate square version (1080x1080) — may need different composition
|
||||
|
||||
Save to: `~/vault/60_Articles/<client-id>/<slug>/images/`
|
||||
|
||||
### Step 5: Present for approval
|
||||
|
||||
```
|
||||
📝 Social posts พร้อมตรวจ:
|
||||
|
||||
📄 Article: [title]
|
||||
🔗 URL: [published_url]
|
||||
|
||||
📱 Platforms: [list from social_platforms]
|
||||
✅ post-facebook.md — [word count] words (if facebook)
|
||||
✅ post-x.md — [format] ([char count] chars) (if x)
|
||||
✅ post-ig.md — [word count] words + image (if instagram)
|
||||
|
||||
ตรวจแล้วเป็นยังไงบ้าง?
|
||||
- ✅ "approve" — อนุมัติ → พร้อม publish
|
||||
- ✏️ "แก้ไข [specify]" — ปรับแก้
|
||||
```
|
||||
|
||||
Wait for approval.
|
||||
|
||||
### Step 6: Save posts
|
||||
|
||||
Save all posts to the article directory:
|
||||
|
||||
```
|
||||
~/vault/60_Articles/<client-id>/<slug>/
|
||||
├── article.md (existing)
|
||||
├── brief.md (existing)
|
||||
├── images/ (existing)
|
||||
├── post-facebook.md ← NEW
|
||||
├── post-x.md ← NEW
|
||||
└── post-ig.md ← NEW
|
||||
```
|
||||
|
||||
### Step 7: Confirm
|
||||
|
||||
```
|
||||
✅ Social posts created:
|
||||
|
||||
📂 ~/vault/60_Articles/<client-id>/<slug>/
|
||||
|
||||
✅ post-facebook.md ← Facebook post ready
|
||||
✅ post-x.md ← X/Twitter post ready
|
||||
✅ post-ig.md ← Instagram post ready
|
||||
|
||||
📋 NEXT: Load mm-publish-content → publish social posts
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
Only creates files for platforms in `social_platforms`:
|
||||
- `~/vault/60_Articles/<client-id>/<slug>/post-facebook.md` (if facebook)
|
||||
- `~/vault/60_Articles/<client-id>/<slug>/post-x.md` (if x)
|
||||
- `~/vault/60_Articles/<client-id>/<slug>/post-ig.md` (if instagram)
|
||||
- Social images in `images/` subdirectory
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **published_url is REQUIRED** — cannot create posts without it
|
||||
- **social_platforms determines which posts to create** — check clients-config.json
|
||||
- If social_platforms is empty, skip social post creation entirely
|
||||
- Social posts do NOT include CTA/contact info
|
||||
- Article URL should be naturally integrated into the post, not just appended
|
||||
- For Instagram, note that links aren't clickable in captions
|
||||
- Each post should complement the article, not duplicate it
|
||||
363
skills/moreminimore-orchestrator/SKILL.md
Normal file
363
skills/moreminimore-orchestrator/SKILL.md
Normal file
@@ -0,0 +1,363 @@
|
||||
---
|
||||
name: moreminimore-orchestrator
|
||||
argument-hint: "<client name> <website URL> [goal] [budget]"
|
||||
description: >
|
||||
Full marketing campaign orchestration skill for moreminimore-service.
|
||||
Chains Research → Strategy → Content phases, passing context between
|
||||
each step. Designed for Hermes agents serving individual clients.
|
||||
Handles client onboarding, keyword research, brand DNA extraction,
|
||||
strategic planning, content writing, and GEO optimization in one
|
||||
coordinated workflow. Use when: starting a new client campaign,
|
||||
running a full marketing workflow, needing end-to-end orchestration,
|
||||
or resuming a partially-completed campaign.
|
||||
---
|
||||
|
||||
# moreminimore Orchestrator
|
||||
|
||||
You are the master orchestrator for moreminimore-service — a coordinated
|
||||
marketing campaign workflow that chains existing Hermes skills together with
|
||||
proper context passing, state tracking, and client folder management.
|
||||
|
||||
## When This Skill Must Trigger
|
||||
|
||||
Trigger on ANY of these signals:
|
||||
- "ลูกค้าใหม่", "client new", "start campaign", "เริ่มแคมเปญ"
|
||||
- "moreminimore orchestrate", "run workflow", "ทำการตลาดให้"
|
||||
- Any request to run a full marketing campaign from scratch
|
||||
- "resume campaign", "continue workflow", "ดูสถานะ"
|
||||
- "รัน workflow", "เริ่มงานให้ [client name]"
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ orchestrator │
|
||||
│ Reads brief → Reads state.json → Determines next step │
|
||||
│ → Calls skill → Saves output → Updates state │
|
||||
│ → Reports progress │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
|
||||
State machine phases (defined in state.json):
|
||||
PHASE_INIT → just started, haven't collected brief
|
||||
PHASE_RESEARCH → running keyword-research + niche-research + ads-dna
|
||||
PHASE_STRATEGY → running ads-plan + ads-create
|
||||
PHASE_CONTENT → running content-writer + geo-optimizer
|
||||
PHASE_COMPLETE → all phases done
|
||||
```
|
||||
|
||||
## Input
|
||||
|
||||
### Required (must be provided or asked):
|
||||
- **Client name** — used as folder name in clients/
|
||||
- **Website URL** — client's website for brand DNA + keyword research
|
||||
|
||||
### Optional (ask if not provided):
|
||||
- **Goal** — sales / leads / brand awareness / traffic
|
||||
- **Budget** — monthly budget range
|
||||
- **Target audience** — who they want to reach
|
||||
- **Primary keyword / topic** — for content focus (if known)
|
||||
|
||||
## Workflow Process
|
||||
|
||||
### Step 0: Check Client Context
|
||||
|
||||
```bash
|
||||
CLIENT_DIR="/Users/kunthawat/Gitea/moreminimore-service-system/clients"
|
||||
```
|
||||
|
||||
If the user provided a client name, check if a folder already exists:
|
||||
|
||||
```bash
|
||||
ls "$CLIENT_DIR/<client-name>/brief.md" 2>/dev/null && echo "EXISTS" || echo "NEW"
|
||||
```
|
||||
|
||||
**If EXISTS**: read the existing brief and state.json. Show the user what's
|
||||
already done and what's pending. Offer to resume or start fresh.
|
||||
|
||||
**If NEW**: proceed to Step 1.
|
||||
|
||||
### Step 1: Collect Client Brief
|
||||
|
||||
Create the client brief template. Ask for (in one message, don't make the user
|
||||
answer one-by-one):
|
||||
|
||||
1. **Client name** (for folder name — use English/lowercase/kebab-case)
|
||||
2. **Website URL**
|
||||
3. **Goal** (default: lead generation)
|
||||
4. **Monthly budget** (default: not specified)
|
||||
5. **Target audience** (default: infer from website)
|
||||
6. **Primary keyword or topic** to focus on (optional)
|
||||
7. **Industry** (e-commerce / SaaS / local service / B2B / content / other)
|
||||
|
||||
Wait for the user's response. Then write the brief:
|
||||
|
||||
```bash
|
||||
mkdir -p "$CLIENT_DIR/<client-name>"
|
||||
```
|
||||
|
||||
Write `clients/<client-name>/brief.md` with the collected info.
|
||||
|
||||
Write `clients/<client-name>/state.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"client_name": "<name>",
|
||||
"website_url": "<url>",
|
||||
"goal": "<goal>",
|
||||
"budget": "<budget or null>",
|
||||
"target_audience": "<audience or null>",
|
||||
"primary_keyword": "<keyword or null>",
|
||||
"industry": "<industry>",
|
||||
"phase": "PHASE_RESEARCH",
|
||||
"phase_progress": {},
|
||||
"outputs": {}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Research Phase (PHASE_RESEARCH)
|
||||
|
||||
Run these three research skills. They can run in parallel since they're
|
||||
independent:
|
||||
|
||||
#### 2a. Brand DNA (ads-dna)
|
||||
|
||||
Use the ads-dna skill to extract brand DNA from the client's website.
|
||||
|
||||
Call flow:
|
||||
1. Fetch the website homepage + about + services pages
|
||||
2. Extract colors, typography, voice, imagery, target audience
|
||||
3. Save as `clients/<client-name>/brand-profile.json`
|
||||
|
||||
**How to invoke ads-dna logic**: The ads-dna skill describes extracting
|
||||
brand elements from a URL. Follow its process:
|
||||
- Fetch homepage URL
|
||||
- Extract colors (CSS, og:image analysis)
|
||||
- Extract typography (Google Fonts, CSS font-family)
|
||||
- Analyze voice (headline, subheadline, CTA text)
|
||||
- Save to brand-profile.json
|
||||
|
||||
Do NOT try to call ads-dna as a sub-skill — follow its documented process
|
||||
directly since you have the skill content loaded.
|
||||
|
||||
#### 2b. Keyword Research (keyword-research)
|
||||
|
||||
Run keyword research for the client's industry/topic.
|
||||
|
||||
Call flow:
|
||||
1. Use the client's primary keyword (or website niche) as seed
|
||||
2. Generate keyword list with intent classification
|
||||
3. Identify topic clusters
|
||||
4. Identify GEO opportunities
|
||||
5. Recommend content priorities
|
||||
|
||||
Save to `clients/<client-name>/keyword-research.md`
|
||||
|
||||
#### 2c. Niche Research (niche-research)
|
||||
|
||||
If the user has browser access (Claude for Chrome or similar), run niche
|
||||
research to find current trending stories. This is optional — skip if no
|
||||
browser is available and note it.
|
||||
|
||||
Save to `clients/<client-name>/niche-research.md` (if ran)
|
||||
|
||||
#### Update state after Research Phase
|
||||
|
||||
```json
|
||||
{
|
||||
...existing state,
|
||||
"phase": "PHASE_STRATEGY",
|
||||
"phase_progress": {
|
||||
"research_completed": true,
|
||||
"brand_profile": "clients/<name>/brand-profile.json",
|
||||
"keyword_research": "clients/<name>/keyword-research.md",
|
||||
"niche_research": "clients/<name>/niche-research.md (or skipped)"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Strategy Phase (PHASE_STRATEGY)
|
||||
|
||||
#### 3a. Campaign Strategy (ads-plan)
|
||||
|
||||
Use ads-plan logic to create a strategic plan:
|
||||
|
||||
1. **Discovery**: Use the client brief + brand DNA
|
||||
2. **Competitive Analysis**: Search for competitors (if web access available)
|
||||
3. **Platform Selection**: Recommend platforms based on industry + goal + budget
|
||||
4. **Campaign Architecture**: Structure with naming conventions
|
||||
5. **Budget Planning**: Budget allocation with 70/20/10 framework
|
||||
6. **Creative Strategy**: Content pillars and creative production plan
|
||||
7. **Implementation Roadmap**: Phased rollout
|
||||
|
||||
Save to `clients/<client-name>/strategy/ADS-STRATEGY.md`
|
||||
|
||||
Also produce:
|
||||
- `clients/<client-name>/strategy/CAMPAIGN-ARCHITECTURE.md`
|
||||
- `clients/<client-name>/strategy/BUDGET-PLAN.md`
|
||||
- `clients/<client-name>/strategy/CREATIVE-BRIEF.md`
|
||||
- `clients/<client-name>/strategy/IMPLEMENTATION-ROADMAP.md`
|
||||
|
||||
#### 3b. Campaign Concepts (ads-create)
|
||||
|
||||
If brand-profile.json exists from Phase 2, generate campaign concepts:
|
||||
|
||||
1. Read brand-profile.json
|
||||
2. Generate 3 campaign concepts with distinct angles
|
||||
3. For each concept: hypothesis, primary message, tone, visual direction, CTA
|
||||
4. Write platform-specific ad copy
|
||||
|
||||
Save to `clients/<client-name>/strategy/campaign-brief.md`
|
||||
|
||||
#### Update state after Strategy Phase
|
||||
|
||||
```json
|
||||
{
|
||||
...existing state,
|
||||
"phase": "PHASE_CONTENT",
|
||||
"phase_progress": {
|
||||
...previous,
|
||||
"strategy_completed": true,
|
||||
"ads_strategy": "clients/<name>/strategy/ADS-STRATEGY.md",
|
||||
"campaign_brief": "clients/<name>/strategy/campaign-brief.md"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Content Phase (PHASE_CONTENT)
|
||||
|
||||
#### 4a. Content Writing (content-writer)
|
||||
|
||||
Using the keyword research (topic clusters + high-priority keywords) and
|
||||
strategy (content pillars), write content:
|
||||
|
||||
1. **Identify priority content**: From keyword-research, pick the top 3-5
|
||||
keywords with best opportunity scores
|
||||
2. **Write blog posts**: For each priority keyword, write an SEO-optimized
|
||||
blog post following content-writer guidelines:
|
||||
- Hook-driven title
|
||||
- ≥1000 words
|
||||
- Featured image + ≥3 inline images (provide image prompts)
|
||||
- FAQ section with FAQPage schema
|
||||
- Internal linking plan
|
||||
3. **Write landing pages**: If the strategy calls for conversion pages,
|
||||
write landing pages optimized for the target keyword and intent
|
||||
|
||||
Save each piece to `clients/<client-name>/content/`:
|
||||
- `clients/<client-name>/content/post-<keyword>.md`
|
||||
- `clients/<client-name>/content/landing-<keyword>.md`
|
||||
|
||||
#### 4b. GEO Optimization (geo-optimizer)
|
||||
|
||||
After writing each piece of content, optimize it for AI search engines:
|
||||
|
||||
1. **Audit** the content for GEO readiness (evidence density, structure, authority)
|
||||
2. **Optimize**: Front-load answers, add real statistics and citations,
|
||||
add FAQPage schema, strip anti-patterns
|
||||
|
||||
Save as an optimization overlay to each content file, or create:
|
||||
- `clients/<client-name>/content/post-<keyword>-geo.md`
|
||||
|
||||
#### 4c. Image Generation Prompts
|
||||
|
||||
For each piece of content, generate detailed image generation prompts:
|
||||
- Featured/thumbnail image prompt
|
||||
- ≥3 inline image prompts with specific dimensions, style, and subject
|
||||
|
||||
These can be used later with image generation tools.
|
||||
|
||||
#### Update state after Content Phase
|
||||
|
||||
```json
|
||||
{
|
||||
...existing state,
|
||||
"phase": "PHASE_COMPLETE",
|
||||
"phase_progress": {
|
||||
...previous,
|
||||
"content_completed": true,
|
||||
"content_pieces": ["clients/<name>/content/post-<keyword1>.md", "..."],
|
||||
"geo_optimized": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 5: Present Summary
|
||||
|
||||
After all phases are complete, present a summary to the user:
|
||||
|
||||
```
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
Campaign Complete: [Client Name]
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
📂 clients/<client-name>/
|
||||
|
||||
✅ RESEARCH PHASE
|
||||
├── brand-profile.json ← Brand DNA extracted
|
||||
├── keyword-research.md ← [N] keywords in [M] clusters
|
||||
└── niche-research.md ← Current trends
|
||||
|
||||
✅ STRATEGY PHASE
|
||||
├── strategy/ADS-STRATEGY.md ← Full strategy
|
||||
├── strategy/CAMPAIGN-ARCHITECTURE.md ← Campaign structure
|
||||
├── strategy/BUDGET-PLAN.md ← Budget allocation
|
||||
├── strategy/CREATIVE-BRIEF.md ← Creative production
|
||||
├── strategy/IMPLEMENTATION-ROADMAP.md ← Timeline
|
||||
└── strategy/campaign-brief.md ← Campaign concepts
|
||||
|
||||
✅ CONTENT PHASE
|
||||
├── content/post-<keyword1>.md ← Blog post + GEO
|
||||
├── content/post-<keyword2>.md ← Blog post + GEO
|
||||
└── content/landing-<keyword>.md ← Landing page + GEO
|
||||
|
||||
📋 NEXT STEPS (available when scripts are ready)
|
||||
1. Deploy content to website
|
||||
2. Set up Google Ads campaigns
|
||||
3. Set up Meta Ads campaigns
|
||||
4. Schedule analytics monitoring
|
||||
```
|
||||
|
||||
## Resuming Workflow
|
||||
|
||||
If the user says "resume", "continue", or "ดูสถานะ":
|
||||
|
||||
1. Read `clients/<client-name>/state.json`
|
||||
2. Check which phase it's in
|
||||
3. Present what's done and what's pending
|
||||
4. Ask if they want to continue from where it left off
|
||||
|
||||
```bash
|
||||
STATE_FILE="/Users/kunthawat/Gitea/moreminimore-service-system/clients/<client-name>/state.json"
|
||||
cat "$STATE_FILE" 2>/dev/null || echo "NOT_FOUND"
|
||||
```
|
||||
|
||||
## Output Convention
|
||||
|
||||
ALL outputs go under:
|
||||
```
|
||||
/Users/kunthawat/Gitea/moreminimore-service-system/clients/<client-name>/
|
||||
```
|
||||
|
||||
Use write_file to save outputs. Always create subdirectories as needed.
|
||||
|
||||
## Important Notes
|
||||
|
||||
1. **Skill availability**: This orchestrator calls skills that exist in the
|
||||
Hermes skill library. If a skill is not loaded or available, note it to
|
||||
the user and skip or approximate the step.
|
||||
|
||||
2. **Parallel execution**: Research skills (keyword-research, niche-research,
|
||||
ads-dna) are independent and CAN run in parallel. Strategy and Content
|
||||
phases are sequential (strategy depends on research, content depends on
|
||||
strategy).
|
||||
|
||||
3. **No real API calls yet**: Google Ads and Meta Ads campaign execution
|
||||
requires API scripts that don't exist yet. The orchestrator produces
|
||||
the campaign structure and copy, but actual ad serving requires the
|
||||
platform API scripts (see scripts/ directory).
|
||||
|
||||
4. **State persistence**: state.json is the source of truth. Always update
|
||||
it after completing each phase. This makes the workflow resumable even
|
||||
if the conversation is interrupted.
|
||||
|
||||
5. **Language**: Use Thai for client-facing summary, English for technical
|
||||
output (unless client brief specifies otherwise).
|
||||
98
skills/post-formatter/SKILL.md
Normal file
98
skills/post-formatter/SKILL.md
Normal file
@@ -0,0 +1,98 @@
|
||||
---
|
||||
name: post-formatter
|
||||
description: >
|
||||
Turn a topic into a ready-to-publish LinkedIn post using PAS, AIDA, BAB, STAR, or SLAY frameworks. 200 to 250 words, 20 lines max, mobile-formatted with blank lines between sentences. Use this skill whenever the user says "format this as a post", "turn this into a LinkedIn post", "write it as PAS" or any named framework, or wants a properly structured post from a topic. Different from post-writer: post-formatter applies a strict framework. post-writer drafts in the user's voice without framework constraints.
|
||||
---
|
||||
|
||||
# Post Formatter
|
||||
|
||||
## CRITICAL: Auto-start on load
|
||||
|
||||
When this skill triggers, go straight to Step 1. Do not summarise. Start input gathering immediately.
|
||||
|
||||
## Step 1. Gather inputs
|
||||
|
||||
Call AskUserQuestion:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"question": "What topic do you want to post about?",
|
||||
"header": "Topic",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{"label": "I will type the topic", "description": "Single sentence describing the subject"},
|
||||
{"label": "Paste a context dump", "description": "Notes, stats, transcripts to turn into a post"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "Which framework?",
|
||||
"header": "Framework",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{"label": "PAS", "description": "Problem, Agitation, Solution"},
|
||||
{"label": "AIDA", "description": "Attention, Interest, Desire, Action"},
|
||||
{"label": "BAB", "description": "Before, After, Bridge"},
|
||||
{"label": "STAR", "description": "Situation, Task, Action, Result"},
|
||||
{"label": "SLAY", "description": "Story, Lesson, Actionable advice, You"},
|
||||
{"label": "Pick for me", "description": "Recommend the best framework based on the topic"}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Ask one follow-up:
|
||||
|
||||
> Anything else I should know? Facts, stats, tone notes, or who this is for.
|
||||
|
||||
Wait for response.
|
||||
|
||||
## Step 2. Write the post
|
||||
|
||||
Apply these global rules to every output:
|
||||
|
||||
- Maximum 20 lines, 200 to 250 words total (~1,200 characters)
|
||||
- Blank line after every line
|
||||
- Most lines: one sentence, 55 characters or fewer
|
||||
- Up to 4 lines may be mini-paragraphs (2 to 3 sentences, 110 characters or fewer)
|
||||
- Grade 6 words. Zero adverbs, zero jargon, zero fluff
|
||||
- No em dashes
|
||||
- No questions unless the hook itself is a question
|
||||
- No emojis except checkmarks for numbered lists (1. 2. 3.) and the recycle symbol in the CTA
|
||||
- Rule of Three: use at most two trios per post
|
||||
- Vary sentence starts. Do not over-use "I"
|
||||
|
||||
## Step 3. Structure
|
||||
|
||||
- **Line 1 (Hook)**: Bold. 50 characters or fewer.
|
||||
- **Line 2 (Twist / Contrast)**: 50 characters or fewer. Opposes or surprises the hook.
|
||||
- **Lines 3 to 18 (Core)**: The chosen framework, split across 3 to 5 lines per stage. Any list inside a stage must have exactly three items (1. 2. 3.). Use arrows to show flow where useful.
|
||||
|
||||
Framework maps:
|
||||
|
||||
- **PAS**: Problem -> Agitation -> Solution
|
||||
- **AIDA**: Attention -> Interest -> Desire -> Action
|
||||
- **BAB**: Before -> After -> Bridge
|
||||
- **STAR**: Situation -> Task -> Action -> Result
|
||||
- **SLAY**: Story -> Lesson -> Actionable advice -> You
|
||||
|
||||
- **Lines 19 to 20 (Wrap and CTA)**: 2 to 3 lines that lock the lesson. Close with one of these phrases followed by the recycle symbol: "Repost if", "Repost this", or "If this helped, repost".
|
||||
|
||||
## Step 4. Output
|
||||
|
||||
Output the finished post inside a code block. No preamble, no trailing notes.
|
||||
|
||||
## Step 5. Offer the next move
|
||||
|
||||
After the post, ask:
|
||||
|
||||
> Want a matching graphic (graphic-designer skill) or want me to score it against your post history (post-scorer skill)?
|
||||
|
||||
## Rules
|
||||
|
||||
- Return the finished post only. No meta-commentary.
|
||||
- Enforce line length, word count, and lines count limits. Count them.
|
||||
- Never use em dashes.
|
||||
- British English unless voice.md specifies otherwise.
|
||||
- If the user has voice.md in the project, tune tone and rhythm to match it.
|
||||
- If a trio is used, it has exactly three items. Not two, not four.
|
||||
432
skills/xurl/SKILL.md
Normal file
432
skills/xurl/SKILL.md
Normal file
@@ -0,0 +1,432 @@
|
||||
---
|
||||
name: xurl
|
||||
description: "X/Twitter via xurl CLI: post, search, DM, media, v2 API."
|
||||
version: 1.1.1
|
||||
author: xdevplatform + openclaw + Hermes Agent
|
||||
license: MIT
|
||||
platforms: [linux, macos]
|
||||
prerequisites:
|
||||
commands: [xurl]
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [twitter, x, social-media, xurl, official-api]
|
||||
homepage: https://github.com/xdevplatform/xurl
|
||||
upstream_skill: https://github.com/openclaw/openclaw/blob/main/skills/xurl/SKILL.md
|
||||
---
|
||||
|
||||
# xurl — X (Twitter) API via the Official CLI
|
||||
|
||||
`xurl` is the X developer platform's official CLI for the X API. It supports shortcut commands for common actions AND raw curl-style access to any v2 endpoint. All commands return JSON to stdout.
|
||||
|
||||
Use this skill for:
|
||||
- posting, replying, quoting, deleting posts
|
||||
- searching posts and reading timelines/mentions
|
||||
- liking, reposting, bookmarking
|
||||
- following, unfollowing, blocking, muting
|
||||
- direct messages
|
||||
- media uploads (images and video)
|
||||
- raw access to any X API v2 endpoint
|
||||
- multi-app / multi-account workflows
|
||||
|
||||
This skill replaces the older `xitter` skill (which wrapped a third-party Python CLI). `xurl` is maintained by the X developer platform team, supports OAuth 2.0 PKCE with auto-refresh, and covers a substantially larger API surface.
|
||||
|
||||
---
|
||||
|
||||
## Secret Safety (MANDATORY)
|
||||
|
||||
Critical rules when operating inside an agent/LLM session:
|
||||
|
||||
- **Never** read, print, parse, summarize, upload, or send `~/.xurl` to LLM context.
|
||||
- **Never** ask the user to paste credentials/tokens into chat.
|
||||
- The user must fill `~/.xurl` with secrets manually on their own machine. In Docker, this must be the `~` seen by Hermes tool subprocesses; see the Docker note below.
|
||||
- **Never** recommend or execute auth commands with inline secrets in agent sessions.
|
||||
- **Never** use `--verbose` / `-v` in agent sessions — it can expose auth headers/tokens.
|
||||
- To verify credentials exist, only use: `xurl auth status`.
|
||||
|
||||
Forbidden flags in agent commands (they accept inline secrets):
|
||||
`--bearer-token`, `--consumer-key`, `--consumer-secret`, `--access-token`, `--token-secret`, `--client-id`, `--client-secret`
|
||||
|
||||
App credential registration and credential rotation must be done by the user manually, outside the agent session. After credentials are registered, the user authenticates with `xurl auth oauth2` — also outside the agent session. Tokens persist to `~/.xurl` in YAML. Each app has isolated tokens. OAuth 2.0 tokens auto-refresh.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
Pick ONE method. On Linux, the shell script or `go install` are the easiest.
|
||||
|
||||
```bash
|
||||
# Shell script (installs to ~/.local/bin, no sudo, works on Linux + macOS)
|
||||
curl -fsSL https://raw.githubusercontent.com/xdevplatform/xurl/main/install.sh | bash
|
||||
|
||||
# Homebrew (macOS)
|
||||
brew install --cask xdevplatform/tap/xurl
|
||||
|
||||
# npm
|
||||
npm install -g @xdevplatform/xurl
|
||||
|
||||
# Go
|
||||
go install github.com/xdevplatform/xurl@latest
|
||||
```
|
||||
|
||||
Verify:
|
||||
|
||||
```bash
|
||||
xurl --help
|
||||
xurl auth status
|
||||
```
|
||||
|
||||
If `xurl` is installed but `auth status` shows no apps or tokens, the user needs to complete auth manually — see the next section.
|
||||
|
||||
---
|
||||
|
||||
## One-Time User Setup (user runs these outside the agent)
|
||||
|
||||
These steps must be performed by the user directly, NOT by the agent, because they involve pasting secrets. Direct the user to this block; do not execute it for them.
|
||||
|
||||
1. Create or open an app at https://developer.x.com/en/portal/dashboard
|
||||
2. Set the redirect URI to `http://localhost:8080/callback`
|
||||
3. Copy the app's Client ID and Client Secret
|
||||
4. Register the app locally (user runs this):
|
||||
```bash
|
||||
xurl auth apps add my-app --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT_SECRET
|
||||
```
|
||||
5. Authenticate (specify `--app` to bind the token to your app):
|
||||
```bash
|
||||
xurl auth oauth2 --app my-app
|
||||
```
|
||||
(This opens a browser for the OAuth 2.0 PKCE flow.)
|
||||
|
||||
If X returns a `UsernameNotFound` error or 403 on the post-OAuth `/2/users/me` lookup, pass your handle explicitly (xurl v1.1.0+):
|
||||
```bash
|
||||
xurl auth oauth2 --app my-app YOUR_USERNAME
|
||||
```
|
||||
This binds the token to your handle and skips the broken `/2/users/me` call.
|
||||
6. Set the app as default so all commands use it:
|
||||
```bash
|
||||
xurl auth default my-app
|
||||
```
|
||||
7. Verify:
|
||||
```bash
|
||||
xurl auth status
|
||||
xurl whoami
|
||||
```
|
||||
|
||||
After this, the agent can use any command below without further setup. OAuth 2.0 tokens auto-refresh.
|
||||
|
||||
> **Common pitfall:** If you omit `--app my-app` from `xurl auth oauth2`, the OAuth token is saved to the built-in `default` app profile — which has no client-id or client-secret. Commands will fail with auth errors even though the OAuth flow appeared to succeed. If you hit this, re-run `xurl auth oauth2 --app my-app` and `xurl auth default my-app`.
|
||||
|
||||
> **Docker HOME pitfall:** In the official Hermes Docker layout, `/opt/data` is `HERMES_HOME`, but Hermes tool subprocesses use `/opt/data/home` as `HOME`. That means `~/.xurl` resolves to `/opt/data/home/.xurl` for Hermes-run `xurl` commands, not `/opt/data/.xurl`. Run the user setup with the same HOME:
|
||||
> ```bash
|
||||
> HOME=/opt/data/home xurl auth apps add my-app --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT_SECRET
|
||||
> HOME=/opt/data/home xurl auth oauth2 --app my-app YOUR_USERNAME
|
||||
> HOME=/opt/data/home xurl auth default my-app YOUR_USERNAME
|
||||
> HOME=/opt/data/home xurl auth status
|
||||
> ```
|
||||
> If `HOME=/opt/data xurl auth status` succeeds but `HOME=/opt/data/home xurl auth status` shows no apps or tokens, Hermes tool calls will not see the credentials.
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Action | Command |
|
||||
| --- | --- |
|
||||
| Post | `xurl post "Hello world!"` |
|
||||
| Reply | `xurl reply POST_ID "Nice post!"` |
|
||||
| Quote | `xurl quote POST_ID "My take"` |
|
||||
| Delete a post | `xurl delete POST_ID` |
|
||||
| Read a post | `xurl read POST_ID` |
|
||||
| Search posts | `xurl search "QUERY" -n 10` |
|
||||
| Who am I | `xurl whoami` |
|
||||
| Look up a user | `xurl user @handle` |
|
||||
| Home timeline | `xurl timeline -n 20` |
|
||||
| Mentions | `xurl mentions -n 10` |
|
||||
| Like / Unlike | `xurl like POST_ID` / `xurl unlike POST_ID` |
|
||||
| Repost / Undo | `xurl repost POST_ID` / `xurl unrepost POST_ID` |
|
||||
| Bookmark / Remove | `xurl bookmark POST_ID` / `xurl unbookmark POST_ID` |
|
||||
| List bookmarks / likes | `xurl bookmarks -n 10` / `xurl likes -n 10` |
|
||||
| Follow / Unfollow | `xurl follow @handle` / `xurl unfollow @handle` |
|
||||
| Following / Followers | `xurl following -n 20` / `xurl followers -n 20` |
|
||||
| Block / Unblock | `xurl block @handle` / `xurl unblock @handle` |
|
||||
| Mute / Unmute | `xurl mute @handle` / `xurl unmute @handle` |
|
||||
| Send DM | `xurl dm @handle "message"` |
|
||||
| List DMs | `xurl dms -n 10` |
|
||||
| Upload media | `xurl media upload path/to/file.mp4` |
|
||||
| Media status | `xurl media status MEDIA_ID` |
|
||||
| List apps | `xurl auth apps list` |
|
||||
| Remove app | `xurl auth apps remove NAME` |
|
||||
| Set default app | `xurl auth default APP_NAME [USERNAME]` |
|
||||
| Per-request app | `xurl --app NAME /2/users/me` |
|
||||
| Auth status | `xurl auth status` |
|
||||
|
||||
Notes:
|
||||
- `POST_ID` accepts full URLs too (e.g. `https://x.com/user/status/1234567890`) — xurl extracts the ID.
|
||||
- Usernames work with or without a leading `@`.
|
||||
|
||||
---
|
||||
|
||||
## Command Details
|
||||
|
||||
### Posting
|
||||
|
||||
```bash
|
||||
xurl post "Hello world!"
|
||||
xurl post "Check this out" --media-id MEDIA_ID
|
||||
xurl post "Thread pics" --media-id 111 --media-id 222
|
||||
|
||||
xurl reply 1234567890 "Great point!"
|
||||
xurl reply https://x.com/user/status/1234567890 "Agreed!"
|
||||
xurl reply 1234567890 "Look at this" --media-id MEDIA_ID
|
||||
|
||||
xurl quote 1234567890 "Adding my thoughts"
|
||||
xurl delete 1234567890
|
||||
```
|
||||
|
||||
### Reading & Search
|
||||
|
||||
```bash
|
||||
xurl read 1234567890
|
||||
xurl read https://x.com/user/status/1234567890
|
||||
|
||||
xurl search "golang"
|
||||
xurl search "from:elonmusk" -n 20
|
||||
xurl search "#buildinpublic lang:en" -n 15
|
||||
```
|
||||
|
||||
For X Articles, use raw API mode instead of the `read` shortcut. `xurl read`
|
||||
expects a post ID or post URL; do not put `read` before a `/2/tweets/...`
|
||||
endpoint. Request the `article` tweet field and ingest `data.article.plain_text`
|
||||
from the JSON response:
|
||||
|
||||
```bash
|
||||
xurl --app APP_NAME '/2/tweets/2057909493250539891?expansions=author_id,attachments.media_keys,referenced_tweets.id&tweet.fields=created_at,lang,public_metrics,context_annotations,entities,possibly_sensitive,conversation_id,in_reply_to_user_id,referenced_tweets,article'
|
||||
```
|
||||
|
||||
### Users, Timeline, Mentions
|
||||
|
||||
```bash
|
||||
xurl whoami
|
||||
xurl user elonmusk
|
||||
xurl user @XDevelopers
|
||||
|
||||
xurl timeline -n 25
|
||||
xurl mentions -n 20
|
||||
```
|
||||
|
||||
### Engagement
|
||||
|
||||
```bash
|
||||
xurl like 1234567890
|
||||
xurl unlike 1234567890
|
||||
|
||||
xurl repost 1234567890
|
||||
xurl unrepost 1234567890
|
||||
|
||||
xurl bookmark 1234567890
|
||||
xurl unbookmark 1234567890
|
||||
|
||||
xurl bookmarks -n 20
|
||||
xurl likes -n 20
|
||||
```
|
||||
|
||||
### Social Graph
|
||||
|
||||
```bash
|
||||
xurl follow @XDevelopers
|
||||
xurl unfollow @XDevelopers
|
||||
|
||||
xurl following -n 50
|
||||
xurl followers -n 50
|
||||
|
||||
# Another user's graph
|
||||
xurl following --of elonmusk -n 20
|
||||
xurl followers --of elonmusk -n 20
|
||||
|
||||
xurl block @spammer
|
||||
xurl unblock @spammer
|
||||
xurl mute @annoying
|
||||
xurl unmute @annoying
|
||||
```
|
||||
|
||||
### Direct Messages
|
||||
|
||||
```bash
|
||||
xurl dm @someuser "Hey, saw your post!"
|
||||
xurl dms -n 25
|
||||
```
|
||||
|
||||
### Media Upload
|
||||
|
||||
```bash
|
||||
# Auto-detect type
|
||||
xurl media upload photo.jpg
|
||||
xurl media upload video.mp4
|
||||
|
||||
# Explicit type/category
|
||||
xurl media upload --media-type image/jpeg --category tweet_image photo.jpg
|
||||
|
||||
# Videos need server-side processing — check status (or poll)
|
||||
xurl media status MEDIA_ID
|
||||
xurl media status --wait MEDIA_ID
|
||||
|
||||
# Full workflow
|
||||
xurl media upload meme.png # returns media id
|
||||
xurl post "lol" --media-id MEDIA_ID
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Raw API Access
|
||||
|
||||
The shortcuts cover common operations. For anything else, use raw curl-style mode against any X API v2 endpoint:
|
||||
|
||||
```bash
|
||||
# GET
|
||||
xurl /2/users/me
|
||||
|
||||
# POST with JSON body
|
||||
xurl -X POST /2/tweets -d '{"text":"Hello world!"}'
|
||||
|
||||
# DELETE / PUT / PATCH
|
||||
xurl -X DELETE /2/tweets/1234567890
|
||||
|
||||
# Custom headers
|
||||
xurl -H "Content-Type: application/json" /2/some/endpoint
|
||||
|
||||
# Force streaming
|
||||
xurl -s /2/tweets/search/stream
|
||||
|
||||
# Full URLs also work
|
||||
xurl https://api.x.com/2/users/me
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Global Flags
|
||||
|
||||
| Flag | Short | Description |
|
||||
| --- | --- | --- |
|
||||
| `--app` | | Use a specific registered app (overrides default) |
|
||||
| `--auth` | | Force auth type: `oauth1`, `oauth2`, or `app` |
|
||||
| `--username` | `-u` | Which OAuth2 account to use (if multiple exist) |
|
||||
| `--verbose` | `-v` | **Forbidden in agent sessions** — leaks auth headers |
|
||||
| `--trace` | `-t` | Add `X-B3-Flags: 1` trace header |
|
||||
|
||||
---
|
||||
|
||||
## Streaming
|
||||
|
||||
Streaming endpoints are auto-detected. Known ones include:
|
||||
|
||||
- `/2/tweets/search/stream`
|
||||
- `/2/tweets/sample/stream`
|
||||
- `/2/tweets/sample10/stream`
|
||||
|
||||
Force streaming on any endpoint with `-s`.
|
||||
|
||||
---
|
||||
|
||||
## Output Format
|
||||
|
||||
All commands return JSON to stdout. Structure mirrors X API v2:
|
||||
|
||||
```json
|
||||
{ "data": { "id": "1234567890", "text": "Hello world!" } }
|
||||
```
|
||||
|
||||
Errors are also JSON:
|
||||
|
||||
```json
|
||||
{ "errors": [ { "message": "Not authorized", "code": 403 } ] }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### Post with an image
|
||||
```bash
|
||||
xurl media upload photo.jpg
|
||||
xurl post "Check out this photo!" --media-id MEDIA_ID
|
||||
```
|
||||
|
||||
### Reply to a conversation
|
||||
```bash
|
||||
xurl read https://x.com/user/status/1234567890
|
||||
xurl reply 1234567890 "Here are my thoughts..."
|
||||
```
|
||||
|
||||
### Search and engage
|
||||
```bash
|
||||
xurl search "topic of interest" -n 10
|
||||
xurl like POST_ID_FROM_RESULTS
|
||||
xurl reply POST_ID_FROM_RESULTS "Great point!"
|
||||
```
|
||||
|
||||
### Check your activity
|
||||
```bash
|
||||
xurl whoami
|
||||
xurl mentions -n 20
|
||||
xurl timeline -n 20
|
||||
```
|
||||
|
||||
### Multiple apps (credentials pre-configured manually)
|
||||
```bash
|
||||
xurl auth default prod alice # prod app, alice user
|
||||
xurl --app staging /2/users/me # one-off against staging
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Non-zero exit code on any error.
|
||||
- API errors are still printed as JSON to stdout, so you can parse them.
|
||||
- Auth errors → have the user re-run `xurl auth oauth2` outside the agent session.
|
||||
- Commands that need the caller's user ID (like, repost, bookmark, follow, etc.) will auto-fetch it via `/2/users/me`. An auth failure there surfaces as an auth error.
|
||||
|
||||
---
|
||||
|
||||
## Agent Workflow
|
||||
|
||||
1. Verify prerequisites: `xurl --help` and `xurl auth status`.
|
||||
2. **Check default app has credentials.** Parse the `auth status` output. The default app is marked with `▸`. If the default app shows `oauth2: (none)` but another app has a valid oauth2 user, tell the user to run `xurl auth default <that-app>` to fix it. This is the most common setup mistake — the user added an app with a custom name but never set it as default, so xurl keeps trying the empty `default` profile.
|
||||
3. If auth is missing entirely, stop and direct the user to the "One-Time User Setup" section — do NOT attempt to register apps or pass secrets yourself.
|
||||
4. Start with a cheap read (`xurl whoami`, `xurl user @handle`, `xurl search ... -n 3`) to confirm reachability.
|
||||
5. Confirm the target post/user and the user's intent before any write action (post, reply, like, repost, DM, follow, block, delete).
|
||||
6. Use JSON output directly — every response is already structured.
|
||||
7. Never paste `~/.xurl` contents back into the conversation.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
| --- | --- | --- |
|
||||
| Auth errors after successful OAuth flow | Token saved to `default` app (no client-id/secret) instead of your named app | `xurl auth oauth2 --app my-app` then `xurl auth default my-app` |
|
||||
| `unauthorized_client` during OAuth | App type set to "Native App" in X dashboard | Change to "Web app, automated app or bot" in User Authentication Settings |
|
||||
| `UsernameNotFound` or 403 on `/2/users/me` right after OAuth | X not returning username reliably from `/2/users/me` | Re-run `xurl auth oauth2 --app my-app YOUR_USERNAME` (xurl v1.1.0+) to pass the handle explicitly |
|
||||
| 401 on every request | Token expired or wrong default app | Check `xurl auth status` — verify `▸` points to an app with oauth2 tokens |
|
||||
| `client-forbidden` / `client-not-enrolled` | X platform enrollment issue | Dashboard → Apps → Manage → Move to "Pay-per-use" package → Production environment |
|
||||
| `CreditsDepleted` | $0 balance on X API | Buy credits (min $5) in Developer Console → Billing |
|
||||
| `media processing failed` on image upload | Default category is `amplify_video` | Add `--category tweet_image --media-type image/png` |
|
||||
| Two "Client Secret" values in X dashboard | UI bug — first is actually Client ID | Confirm on the "Keys and tokens" page; ID ends in `MTpjaQ` |
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- **Rate limits:** X enforces per-endpoint rate limits. A 429 means wait and retry. Write endpoints (post, reply, like, repost) have tighter limits than reads.
|
||||
- **Scopes:** OAuth 2.0 tokens use broad scopes. A 403 on a specific action usually means the token is missing a scope — have the user re-run `xurl auth oauth2`.
|
||||
- **Token refresh:** OAuth 2.0 tokens auto-refresh. Nothing to do.
|
||||
- **Multiple apps:** Each app has isolated credentials/tokens. Switch with `xurl auth default` or `--app`.
|
||||
- **Multiple accounts per app:** Select with `-u / --username`, or set a default with `xurl auth default APP USER`.
|
||||
- **Token storage:** `~/.xurl` is YAML. In Docker, use the Hermes subprocess HOME (`/opt/data/home` in the official image) so tokens land under `/opt/data/home/.xurl`. Never read or send this file to LLM context.
|
||||
- **Cost:** X API access is typically paid for meaningful usage. Many failures are plan/permission problems, not code problems.
|
||||
|
||||
---
|
||||
|
||||
## Attribution
|
||||
|
||||
- Upstream CLI: https://github.com/xdevplatform/xurl (X developer platform team, Chris Park et al.)
|
||||
- Upstream agent skill: https://github.com/openclaw/openclaw/blob/main/skills/xurl/SKILL.md
|
||||
- Hermes adaptation: reformatted for Hermes skill conventions; safety guardrails preserved verbatim.
|
||||
Reference in New Issue
Block a user