← 返回 Skills 市场
🔌

Seo Blog Writer

作者 AutomateLab · GitHub ↗ · v2.2.0 · MIT-0
cross-platform ✓ 安全检测通过
51
总下载
0
收藏
0
当前安装
1
版本数
在 OpenClaw 中安装
/install automatelab-seo-blog-writer
功能描述
Turn a single long-tail query into a publish-ready blog post that ranks in search and gets quoted by AI assistants. Runs the full pipeline: classify the topi...
使用说明 (SKILL.md)

\r \r

seo-blog-writer\r

\r End-to-end pipeline for shipping a single long-tail blog post: topic -> research -> draft -> scrub -> AI-SEO audit -> publish. Designed for SEO and AI-citation extractability (FAQ blocks, BreadcrumbList + FAQPage + HowTo schema, query-phrased headings).\r \r The writing pipeline is platform-agnostic — it produces a publish-ready bundle (clean HTML, slug, meta, JSON-LD blocks, feature-image alt). The publish step is pluggable: out-of-the-box adapters for Ghost Admin API, WordPress REST, and static-site file output. Adding another CMS (Webflow, Sanity, Strapi, Contentful, Hugo, Astro) is a matter of writing a 20-line POST snippet.\r \r The skill takes one required argument: the topic. Optional flags control the publish target and state.\r \r

/seo-blog-writer \x3Ctopic>\r
/seo-blog-writer \x3Ctopic> --target ghost                     # publish via Ghost adapter\r
/seo-blog-writer \x3Ctopic> --target wordpress                 # publish via WordPress REST\r
/seo-blog-writer \x3Ctopic> --target static --out posts/       # write files into a static-site repo\r
/seo-blog-writer \x3Ctopic> --target ghost --publish           # actually publish (default: draft)\r
/seo-blog-writer \x3Ctopic> --target ghost --publish-at \x3CISO>  # schedule for future publish\r
/seo-blog-writer \x3Ctopic> --angle "\x3Cangle>"                  # narrow the angle\r
```\r
\r
Default state is **draft** — the post lands in the platform's editor for human review before going live, unless `--publish` or `--publish-at` is passed. `--publish-at` accepts an ISO 8601 UTC timestamp (e.g. `2026-05-10T07:42:00Z`) and is mutually exclusive with `--publish`.\r
\r
Default `--target` is `static` — writes a self-contained HTML file + a `metadata.json` next to it so you can wire any platform yourself.\r
\r
---\r
\r
## Before you start — preflight\r
\r
The platform-agnostic checks:\r
\r
```bash\r
# 1. Python available (rasterizer, scrubber, schema builder)\r
command -v python3\r
\r
# 2. Working directory writable\r
mkdir -p tmp/blog-drafts && touch tmp/blog-drafts/.touch && rm tmp/blog-drafts/.touch\r
```\r
\r
**3. (Optional) ai-seo MCP — check before continuing**\r
\r
Check whether the current agent session has access to a tool named `audit_page` from the ai-seo-mcp server (`@automatelab/ai-seo-mcp`). That MCP provides a programmatic citation-worthiness and schema score that Step 5 uses automatically when available.\r
\r
- **If the MCP is connected:** nothing to do — Step 5 will call `audit_page` automatically.\r
- **If the MCP is not connected:** ask the user:\r
\r
  > "The **ai-seo MCP** (`@automatelab/ai-seo-mcp`) is not connected. Step 5 can run a programmatic citation-worthiness and schema score on your draft in addition to the manual audit. To install it:\r
  > ```\r
  > npx -y @automatelab/ai-seo-mcp\r
  > ```\r
  > then register it in your MCP config. See the [ai-seo-mcp README](https://github.com/AutomateLab-tech/ai-seo-mcp) for one-line configs for Claude Code, Cursor, and Cline. Type **skip** to continue with the manual-only audit."\r
\r
  Wait for the user's response before continuing to Step 0. Any response other than a config/install action counts as skip — proceed without the MCP.\r
\r
Platform-specific credential checks live in the per-adapter sections at the end of this skill. The writing pipeline (Steps 0-7) runs without any platform credentials — credentials are only needed at Step 8.\r
\r
---\r
\r
## Step 0 — Parse and classify the topic\r
\r
The topic is the one thing the skill cannot invent. It must arrive as an argument.\r
\r
| Shape | Example | Treatment |\r
|---|---|---|\r
| **Long-tail how-to** | `"how to fix n8n HTTP Request 401 error"` | Ideal. Format = troubleshooting (template 1). |\r
| **Integration walk-through** | `"how to connect Airtable to Slack with Zapier"` | Format = integration (template 2). |\r
| **Workflow tutorial** | `"automate invoice processing with Make"` | Format = workflow tutorial (template 3). |\r
| **Comparison** | `"Zapier vs Make vs n8n"` | Format = comparison (template 4). |\r
| **Definition / explainer** | `"what is an AI agent"` | Format = explainer (template 5). |\r
| **Use case / outcome** | `"build a daily Slack digest from RSS with n8n"` | Format = use-case (template 6). |\r
| **Listicle / roundup** | `"12 best n8n templates for marketing teams"` | Format = listicle (template 7). |\r
| **Migration guide** | `"migrate from Zapier to n8n"` | Format = migration (template 8). |\r
| **Release recap** | `"what's new in n8n 1.80"` | Format = release-recap (template 9). |\r
| **Too vague** | `"AI"`, `"automation"` | **Stop.** Ask the user to narrow it. Suggest 2-3 candidate long-tail variants. |\r
\r
If `--angle` was passed, append it to the topic. The classification picks the structural template used in Step 3.\r
\r
---\r
\r
## Step 1 — Research\r
\r
The piece must be specific. Real version numbers, real error messages, real screenshots — not generic "best practices."\r
\r
### 1a. Identify the search intent\r
\r
What does someone typing this query want? One sentence — the implicit desire behind the words.\r
\r
- `"how to fix n8n HTTP 401"` -> wants the exact change to make in the UI to stop the error\r
- `"Zapier vs Make"` -> wants a quick decision, then a longer breakdown\r
- `"what is an AI agent"` -> wants a one-paragraph explanation, then how it differs from a workflow\r
\r
If you can't write one sentence describing the intent, the topic is too vague — go back to Step 0.\r
\r
### 1b. Seed search and SERP teardown\r
\r
```\r
WebSearch("\x3Ctopic>")\r
WebSearch("\x3Ctopic> \x3Ccurrent-year>")  # force a fresh lens\r
```\r
\r
Extract three structured signals from the page-1 results:\r
\r
1. **Word count distribution** — eyeball the top 5 results' length. Target 1.1–1.3x the median, not the longest. If the median is 600 words, don't write 1500 — that's padding.\r
2. **People Also Ask boxes** — Google surfaces 4-8 PAA questions for most queries. These are free FAQ content. Capture verbatim into the FAQ-variant list.\r
3. **Currently-winning featured snippet** — if there is one, note its format (paragraph, list, table). Write the lead paragraph in that exact shape; that's how you challenge for the snippet.\r
\r
Goal: write something **more specific or more current** than the existing top results, not a paraphrase.\r
\r
### 1c. Deep fetch\r
\r
Pick **2-4 URLs** from the SERP. Prioritize:\r
\r
- **Vendor docs** — primary sources for the tool being discussed.\r
- **GitHub issues / changelogs** — for "fix X error" topics, the actual issue thread is gold.\r
- **Reddit / community forums** — for confirming a workaround actually works in the wild.\r
- **Existing top-ranked posts** — to see the bar you're clearing.\r
\r
```\r
WebFetch(url, "Return the full article body as clean prose. Include code snippets,\r
error messages, and screenshot references verbatim. Do NOT summarize.")\r
```\r
\r
Skip SEO-farm rewrites and listicles with no specifics.\r
\r
### 1d. Five-question gate before drafting\r
\r
Before writing, you must be able to answer all five.\r
\r
1. **What is the exact query intent?** (one sentence from 1a)\r
2. **What is the direct answer?** (one to two sentences — the lead paragraph in compressed form)\r
3. **What's the canonical primary source?** (vendor doc, GitHub issue, official changelog — at least one URL)\r
4. **What's the gotcha most existing posts miss?** (the specific detail that makes this post worth writing). **Hard rule:** if the honest answer is "nothing, I'm summarizing the docs," **abort and tell the user**. A doc paraphrase will rank below the actual docs.\r
5. **What 3-6 follow-on questions belong in the FAQ?** (long-tail variations of the main query, ideally lifted from the PAA boxes captured in 1b)\r
\r
If any answer is `?`, keep researching or ask the user for a specific source.\r
\r
### 1e. Save research artifacts\r
\r
```bash\r
mkdir -p tmp/blog-drafts\r
# \x3Cslug> = kebab-case of the topic, e.g. n8n-http-401-fix\r
```\r
\r
Files (gitignored):\r
- `tmp/blog-drafts/\x3Cslug>.research.md` — 5-question answers, source list, key quotes\r
- `tmp/blog-drafts/\x3Cslug>.interlinks.json` — written in Step 1f (outbound interlink targets)\r
- `tmp/blog-drafts/\x3Cslug>.draft.html` — written in Step 3\r
- `tmp/blog-drafts/\x3Cslug>.schema.html` — written in Step 7b (JSON-LD `\x3Cscript>` blocks)\r
- `tmp/blog-drafts/\x3Cslug>.metadata.json` — written in Step 7f (title, slug, tags, meta, etc.)\r
- `tmp/blog-drafts/\x3Cslug>.refresh.json` — written in Step 7h (versions, prices, years cited; for future refresh runs)\r
\r
### 1f. Outbound interlinks (recommended; required for >800-word posts)\r
\r
Pick **2-3 prior posts** on the same site whose topic genuinely overlaps with this one. Bake the links into the draft in Step 3 on topical noun phrases (not "see this post"). Internal links don't carry `nofollow`; outbound links to other domains do (see Step 3 link policy).\r
\r
Where the candidate list comes from depends on the platform:\r
\r
- **Ghost** — `GET /ghost/api/admin/posts/?limit=all&filter=status:published&fields=id,slug,title,published_at,custom_excerpt&order=published_at%20desc` (same `GHOST_ADMIN_KEY` Step 8 uses).\r
- **WordPress** — `GET /wp-json/wp/v2/posts?per_page=100&_fields=id,slug,title,date,excerpt&orderby=date&order=desc` (same `WP_APP_PASSWORD` Step 8 uses).\r
- **Static-site** — read the SSG's content directory directly (`ls content/posts/*.md`) or maintain a hand-curated `posts-inventory.json` in the repo.\r
\r
Save the chosen targets so Step 3 can splice them in and Step 7g can verify they survived the audit:\r
\r
```bash\r
cat > tmp/blog-drafts/\x3Cslug>.interlinks.json \x3C\x3C'EOF'\r
{\r
  "outbound": [\r
    {"slug": "\x3Cprior-slug-1>", "url": "https://\x3Cyour-host>/\x3Cprior-slug-1>/", "anchor_phrase": "\x3Cnoun phrase>"},\r
    {"slug": "\x3Cprior-slug-2>", "url": "https://\x3Cyour-host>/\x3Cprior-slug-2>/", "anchor_phrase": "\x3Cnoun phrase>"}\r
  ]\r
}\r
EOF\r
```\r
\r
Step 7g verifies that every `outbound[].url` appears at least once as an `href` in the final draft. If you decided mid-draft to drop a link, edit the file before re-running 7g. Posts under 800 words can skip this step; long posts ship with outbound links or they look orphaned to both the reader and the site graph.\r
\r
> **Note on inbound links.** Editing prior posts after publish to add a forward link back to the new one (inbound splicing) is a separate concern that depends on having write access to historical posts and a state file to keep the operation idempotent. This skill does not handle it — too platform-specific to generalize. If you want it, run it as a cron against your platform's API after publish.\r
\r
---\r
\r
## Step 2 — Pick the format and length band\r
\r
Each query type maps to a structural template:\r
\r
| Format | Length band |\r
|---|---|\r
| `how-to-fix` (troubleshooting) | 600-1200 |\r
| `how-to-connect` (integration) | 1000-1500 |\r
| `how-to-automate` (workflow) | 1000-1500 |\r
| `x-vs-y` (comparison) | 1200-1500 |\r
| `what-is` (explainer) | 600-1200 |\r
| `use-case` (outcome) | 1000-1500 |\r
| `listicle` (roundup) | 1500-2500 |\r
| `migration` | 1200-1800 |\r
| `release-recap` | 800-1400 |\r
\r
**Hard length range: 600-1500 words for most formats.** Word count = prose inside `\x3Cp>` tags + heading text. Excludes code blocks, table cells, figcaptions.\r
\r
Use the SERP word-count signal from Step 1b to pick a target inside the band (1.1–1.3x the SERP median). Under the floor means the answer is genuinely too thin — add an FAQ expansion, a "common errors" section, or a "how to verify" section. Over the ceiling means the post is sprawling — cut the weakest section. **Never pad to hit a floor.** Google rewards directness; AI Overviews preferentially extract from concise answers.\r
\r
---\r
\r
## Step 3 — Draft the post\r
\r
Write directly in HTML. Allowed tags:\r
\r
`\x3Cp>`, `\x3Ch2>`, `\x3Ch3>`, `\x3Ca>`, `\x3Cstrong>`, `\x3Cem>`, `\x3Ccode>`, `\x3Cpre>`, `\x3Cblockquote>`, `\x3Cul>`, `\x3Col>`, `\x3Cli>`, `\x3Ctable>`, `\x3Cthead>`, `\x3Ctbody>`, `\x3Ctr>`, `\x3Cth>`, `\x3Ctd>`, `\x3Cfigure>`, `\x3Cfigcaption>`, `\x3Cimg>`.\r
\r
No inline styles. No `\x3Cdiv>`, no `\x3Cspan>`, no `\x3Cbr>`. No H1 (most platforms emit the post title as H1; emitting your own creates a duplicate).\r
\r
### Link policy — internal vs. outbound, follow vs. nofollow\r
\r
| Destination | `rel` attribute |\r
|---|---|\r
| Your own blog (other posts on the same host) | none — internal, follow |\r
| Anything else (vendor docs, GitHub, news, social, all third-party) | `rel="nofollow noopener"` |\r
\r
Do not use `target="_blank"` — most blog themes handle outbound link UX themselves. Set `CANONICAL_HOST=blog.example.com` in the shell before running the audit in Step 5 so the validator knows which links are internal.\r
\r
### Voice checks while drafting\r
\r
- **Open with a TL;DR block.** First child of the body is `\x3Cp>\x3Cstrong>TL;DR:\x3C/strong> ...\x3C/p>` — a single sentence, 8-40 words, that answers the query directly with specific nouns (tool name, version, error code, command). LLM citation hook. Asserted in Step 7g.\r
- **Lead paragraph follows the TL;DR** with one or two sentences of context (when this hits, who it bites, why other guides miss the cause). It is not a re-statement of the answer.\r
- **H2 as a question or operational label.** Every `\x3Ch2>` either ends with `?` (e.g. `## How do you fix the "ECONNREFUSED" error in n8n?`) **or** is one of the allowlist: `Install`, `Prerequisites`, `Links`, `TL;DR`, `FAQ`, `Frequently asked questions`, `Summary`, `References`, `Further reading`, `Sources`, `Bottom line`. `\x3Ch3>` follows the same convention. Question-shaped H2s are how Google AI Overviews and Perplexity slice the page into citable chunks. Asserted in Step 7g.\r
- **Specific over general.** Real version numbers, real error messages, real prices. No "modern", "powerful", "robust", "seamless."\r
- **Impersonal voice.** "Here's the fix." Not "we found that" and not "I tried this."\r
- **Forensic linking.** Every external claim links on the noun phrase that names the source. Every external link has `rel="nofollow noopener"`.\r
- **Bullet discipline.** No `\x3Cul>` or `\x3Col>` under 3 items — convert to prose. No list over 9 items without a sub-grouping (split into 2 lists under separate H3s, or fold into a `\x3Ctable>`). Every `\x3Cli>` carries a data point, recommendation, or argument; each ends with a period; parallel grammar across items. Asserted in Step 7g.\r
- **Structured-spec labels for diagnostic posts.** Troubleshooting roundups, "N reasons X is broken", and cause/effect listicles repeat a labeled triple inside every item — the default is `**Symptom:**` / `**Diagnostic:**` / `**Fix:**` (one paragraph each). The bold-keyword-colon form is allowed here and only here. For migration posts use `**Before:**` / `**After:**` / `**Migration step:**`; for comparison posts use `**When to pick:**` / `**Avoid if:**` / `**Cost:**`. This is what gets AI assistants to extract per-item structured citations instead of mashing the whole list into one quote.\r
- **Recap checklist before the FAQ for enumerative posts.** Posts with **three or more enumerated items** close with an `\x3Col>` of one-sentence imperative steps under a question-shaped H2 (e.g. `\x3Ch2>How do you test all seven blockers in 20 minutes?\x3C/h2>`). One step per body item, no sub-bullets. Skip for posts under 800 words or fewer than three items. The recap is what gets quoted as the AI-answer "summary" — without it the model has to invent one.\r
- **Currency where it matters.** Any version number, year, or price in a load-bearing claim either is current (cross-check against vendor docs in Step 5) or carries `as of \x3CYYYY-MM>` next to it so a reader knows the time-context. Step 7g flags any year > 1 year stale without an explicit `as of` qualifier.\r
- **End with a `\x3Ch2>FAQ\x3C/h2>` block** — 3-6 H3 questions, each with a 1-3 sentence answer.\r
- **Self-check:** *Does the TL;DR stand alone as a quotable answer? Does the lead paragraph add context the TL;DR doesn't have? If either fails, rewrite.*\r
\r
Save to `tmp/blog-drafts/\x3Cslug>.draft.html`.\r
\r
---\r
\r
## Step 4 — Scrub LLM tells\r
\r
Run **before** the AI-SEO audit. The audit may add vocabulary the scrub would then need to remove; do the order this way.\r
\r
### 4a. Character scrub (automatic)\r
\r
Replace common LLM-tell characters with ASCII equivalents:\r
\r
```bash\r
python3 -c "\r
import sys, pathlib\r
p = pathlib.Path(sys.argv[1])\r
t = p.read_text(encoding='utf-8')\r
# em-dash/en-dash -> hyphen\r
t = t.replace('—', '-').replace('–', '-')\r
# smart quotes -> straight quotes\r
t = t.replace('“', '\"').replace('”', '\"')\r
t = t.replace('‘', \"'\").replace('’', \"'\")\r
# ellipsis -> three dots\r
t = t.replace('…', '...')\r
# zero-width / non-breaking space -> regular space or empty\r
t = t.replace('​', '').replace(' ', ' ')\r
p.write_text(t, encoding='utf-8')\r
print('scrubbed', sys.argv[1])\r
" tmp/blog-drafts/\x3Cslug>.draft.html\r
```\r
\r
### 4b. Prose-level tells (manual)\r
\r
Search the draft for these banned phrases and rewrite:\r
\r
- "delve into", "delving"\r
- "in today's fast-paced world", "in the ever-evolving"\r
- "robust", "seamless", "powerful", "cutting-edge"\r
- "harness the power of"\r
- "it's worth noting that", "it's important to note"\r
- "navigate the landscape", "navigating the complexities"\r
- "unlock the potential of", "unleash"\r
- "game-changer", "revolutionize"\r
- "leverage" (as a verb)\r
\r
Rewrite every hit — do not just delete; the surrounding sentence is usually also lazy.\r
\r
---\r
\r
## Step 5 — AI-SEO audit\r
\r
### Programmatic pass (if ai-seo-mcp is connected)\r
\r
If the ai-seo-mcp server is connected, call `audit_page` on the draft before running the manual passes:\r
\r
```\r
audit_page(url_or_path="tmp/blog-drafts/\x3Cslug>.draft.html")\r
```\r
\r
Feed the score and any flagged issues into the manual passes below as additional signal. The MCP output is advisory — the six manual passes are still required gates.\r
\r
### Manual passes\r
\r
Run the audit against the draft, checking each pass:\r
\r
1. **Structure pass** — does the lead answer the query in the first paragraph; do H2s match query phrasing; is each section self-contained.\r
2. **Authority pass** — at least one cited primary source (vendor doc / GitHub issue / changelog) on a relevant noun phrase.\r
3. **Freshness pass** — current year referenced where it makes sense; version numbers are current. **Currency check, mandatory:** any version number cited must still be the current (or one of the still-supported) versions per vendor docs. A 6-month-old "introduced in CrewAI 0.114" may now read as historical context, not present-tense scope. If the version has rolled forward, either update the framing or add `as of \x3CYYYY-MM>` next to the claim so the reader knows the time-context. Vendors ship fast; stale qualifiers tank citation quality.\r
4. **Schema readiness** — most platforms emit Article + Person + Organization schema automatically. Step 7b adds FAQPage + BreadcrumbList (always) and HowTo (procedural posts only). Confirm the FAQ block has H3 question + paragraph answer pairs the 7b extractor can parse.\r
5. **Long-tail coverage** — does the FAQ block capture 3-6 long-tail variants of the main query.\r
6. **Platform-fact pass** — any claim about a specific shell, OS, language runtime, or tool is a verifiable fact, not a vibe. Verify the load-bearing ones against vendor docs before publish.\r
\r
Apply recommendations **in place** in the draft, then re-run Step 4a (the audit may have re-introduced smart quotes).\r
\r
### Non-negotiable invariants\r
\r
- **Body is within the format's length band** (Step 2). Count via the snippet below.\r
- **TL;DR is the first `\x3Cp>` of the body**, opens with `\x3Cstrong>TL;DR:\x3C/strong>`, 8-40 words, single sentence.\r
- **Lead paragraph (second `\x3Cp>`) answers the query** in 1-2 sentences.\r
- **At least one primary-source link** with `rel="nofollow noopener"`.\r
- **FAQ block at the end** with 3-6 H3/p pairs.\r
- **Every external `\x3Ca>` carries `rel="nofollow noopener"`.**\r
- **Zero U+2014, U+201C, U+201D, U+2018, U+2019, U+2026, U+00A0, U+200B.**\r
\r
```bash\r
# Word count (excludes code blocks, table cells, figcaptions)\r
python3 -c "\r
import sys, re, pathlib\r
html = pathlib.Path(sys.argv[1]).read_text(encoding='utf-8')\r
no_code = re.sub(r'\x3Cpre\b[^>]*>.*?\x3C/pre>', ' ', html, flags=re.S|re.I)\r
no_table = re.sub(r'\x3Ctable\b[^>]*>.*?\x3C/table>', ' ', no_code, flags=re.S|re.I)\r
no_fig = re.sub(r'\x3Cfigure\b[^>]*>.*?\x3C/figure>', ' ', no_table, flags=re.S|re.I)\r
text = re.sub(r'\x3C[^>]+>', ' ', no_fig)\r
words = re.findall(r\"[A-Za-z0-9][A-Za-z0-9'-]*\", text)\r
print(f'{len(words)} words')\r
" tmp/blog-drafts/\x3Cslug>.draft.html\r
```\r
\r
```bash\r
# nofollow coverage on external links — expected: 0 violations.\r
# Set CANONICAL_HOST to your blog's hostname (e.g. blog.example.com).\r
python3 -c "\r
import re, sys, pathlib, os\r
from urllib.parse import urlparse\r
html = pathlib.Path(sys.argv[1]).read_text(encoding='utf-8')\r
host = os.environ.get('CANONICAL_HOST', '')\r
internal = {host, f'www.{host}' if host else ''}\r
internal = {h for h in internal if h}\r
violations = []\r
for m in re.finditer(r'\x3Ca\b([^>]*)>', html, flags=re.I):\r
    attrs = m.group(1)\r
    href = re.search(r'href=\"([^\"]+)\"', attrs, flags=re.I)\r
    if not href: continue\r
    h = urlparse(href.group(1)).hostname or ''\r
    if h and h not in internal:\r
        rel = re.search(r'rel=\"([^\"]+)\"', attrs, flags=re.I)\r
        rel_val = (rel.group(1) if rel else '').lower()\r
        if 'nofollow' not in rel_val:\r
            violations.append(href.group(1))\r
for v in violations: print('MISSING nofollow:', v)\r
print(f'{len(violations)} violation(s)')\r
" tmp/blog-drafts/\x3Cslug>.draft.html\r
```\r
\r
---\r
\r
## Step 6 — Illustrate the post (optional)\r
\r
Figures are not required for short posts, but **mandatory for posts >=800 words**. The rule: `figures >= max(1, words // 500)` whenever body word count >=800. An 800-word post -> 1-2 figures. A 1200-word post -> 2-3. A 1500-word post -> 3. Step 7g asserts this. Past failure mode this rule is fixing: long troubleshooting posts (1000+ words) shipped with zero figures because the agent declared the topic "too definitional" — the assert refuses those bundles.\r
\r
For figure generation (SVG flow diagrams, comparison charts, taxonomy diagrams, OG feature cards) see the companion `blog-figure-svg` skill — it generates accessible SVGs with consistent styling and rasterizes them for upload. The skill is CMS-agnostic; it produces PNG files that any adapter in Step 8 can upload.\r
\r
For screenshots, capture from the live tool (Playwright, real session, etc.), crop to the relevant region, redact tokens or personal data. Save as `tmp/blog-drafts/\x3Cslug>-\x3CN>-\x3Cshort-name>.png`.\r
\r
### Splice figure tags into the draft\r
\r
```html\r
\x3Cfigure>\r
  \x3Cimg src="\x3Cimage-url-or-path>" alt="\x3Cfull description with all numbers and labels>" loading="lazy">\r
  \x3Cfigcaption>One sentence restating the takeaway in plain English (15-30 words).\x3C/figcaption>\r
\x3C/figure>\r
```\r
\r
**Caption rules:**\r
- Required on every figure. Step 7g asserts this.\r
- 15-30 words, restating the takeaway (not "Figure showing X" — say what the reader should conclude).\r
- Allowed tags inside `\x3Cfigcaption>`: `\x3Ca>` (with `rel="nofollow noopener"` for external), `\x3Cem>`.\r
\r
The `\x3Cimg src>` value depends on the publish target:\r
- **Ghost / WordPress**: upload first (per-adapter snippet in Step 8), then splice the returned CDN URL.\r
- **Static-site**: copy the PNG into the site's image directory and use a relative path.\r
\r
---\r
\r
## Step 7 — Build the publish bundle\r
\r
The bundle is three files that every adapter consumes:\r
\r
| File | Contents |\r
|---|---|\r
| `\x3Cslug>.draft.html` | Body HTML (already produced in Step 3, scrubbed and audited). |\r
| `\x3Cslug>.schema.html` | JSON-LD `\x3Cscript>` blocks (FAQPage + BreadcrumbList + optional HowTo). |\r
| `\x3Cslug>.metadata.json` | Title, slug, tags, author, meta title/description, excerpt, feature image, status, publish-at. |\r
\r
### 7a. Headline and slug rules\r
\r
**Headline** (becomes the SEO title unless `meta_title` overrides):\r
\r
- Under **70 chars**.\r
- Match the search query closely.\r
- Lead with the verb / noun the searcher typed.\r
\r
**Slug** (URL fragment):\r
\r
- **\x3C=60 chars.**\r
- **Strip stop words** — drop `the`, `a`, `an`, `for`, `with`, `in`, `to`, `of`, `on`, `and`, `or`, `is`, `are`.\r
- **No version numbers** — `n8n-1-45-2-fix` goes stale; `n8n-http-401-fix` does not.\r
- **Match the primary keyword**, not the full headline.\r
\r
```python\r
import re\r
STOP = {'the','a','an','for','with','in','to','of','on','and','or','is','are'}\r
slug = "-".join(t for t in re.findall(r'[a-z0-9]+', topic.lower()) if t not in STOP)\r
slug = slug[:60].rstrip('-')\r
```\r
\r
### 7b. Build JSON-LD schema (FAQPage + BreadcrumbList + HowTo)\r
\r
Most platforms emit Article/BlogPosting/Person/Organization schema by default. This skill **adds three more** for AI-citation extractability:\r
\r
- **FAQPage** — mandatory. Every post has a FAQ block (Step 3 rule).\r
- **BreadcrumbList** — mandatory. `Home > \x3CPrimary Tag> > \x3CPost Title>`.\r
- **HowTo** — only for procedural formats with >=3 step-shaped H2s.\r
\r
**Critical gotcha for rich-text editors:** several CMSes (Ghost's Lexical, WordPress's block editor under some configurations) convert the source HTML into a structured format on save and silently drop `\x3Cscript>` nodes — so JSON-LD inlined in the draft body **disappears in the live page** even though it was present in the POST payload.\r
\r
The blocks must go in a platform-specific "head injection" slot:\r
\r
| Platform | Where the schema goes |\r
|---|---|\r
| Ghost | `codeinjection_head` field on the post payload |\r
| WordPress | `\x3Chead>` via a theme hook, or the Yoast / Rank Math "schema" panel |\r
| Static-site | written directly into the rendered HTML's `\x3Chead>` by your build step |\r
\r
**Never append `\x3Cscript type="application/ld+json">` to the body HTML.** Build it once via this step into `\x3Cslug>.schema.html`; the platform adapter in Step 8 reads that file and writes it into the correct field.\r
\r
```bash\r
# Args: slug, headline, format, primary-tag-name, canonical-base-url\r
python3 scripts/seo-blog-writer/build-schema.py "\x3Cslug>" "\x3Cheadline>" "\x3Cformat>" "\x3Cprimary-tag>" "\x3Ccanonical-base-url>"\r
```\r
\r
### 7c. Feature image (recommended)\r
\r
A feature image is shown at the top of the post and as the OG image in social shares. Strongly recommended for any post you intend to promote.\r
\r
Options:\r
- **Upload a custom image** — per-adapter upload snippets are in Step 8.\r
- **Generate a templated title card** — see the companion `blog-figure-svg` skill (`feature` variant) for a 1600x840 OG card with a clean headline + brand mark.\r
- **Skip it** — the post will render without a hero image; social previews fall back to the site default.\r
\r
Whatever path you pick, capture the URL (or filesystem path for static targets) plus a one-line alt-text in `metadata.json`. **Cap alt text at 191 chars** — Ghost silently truncates at varchar(191), and the limit is a reasonable upper bound for any platform.\r
\r
### 7d. Author byline\r
\r
Every post needs an author. The shape varies by platform; capture it generically in metadata:\r
\r
```json\r
"author": {"slug": "\x3Cauthor-slug>", "name": "\x3Cdisplay name>"}\r
```\r
\r
The adapter in Step 8 translates this to the platform's API shape:\r
- **Ghost** — `authors: [{"slug": "\x3Cslug>"}]`. Slug must match an existing user; otherwise Ghost silently substitutes the integration owner.\r
- **WordPress** — `author: \x3Cuser-id>` (numeric). Resolve slug -> id once and cache.\r
- **Static-site** — written into the front-matter `author:` field of the generated file.\r
\r
### 7e. Tags\r
\r
Use a flat list of tag name strings:\r
\r
```json\r
"tags": ["How To", "n8n"]\r
```\r
\r
**Pick 1-3 tags per post.** The first tag is the **primary tag** — it becomes the breadcrumb segment in 7b and is used by most themes for category labelling.\r
\r
Maintain a small canonical tag list in your project (don't let the AI invent new tags every post — duplicates dilute SEO). Common patterns: format tags (`How To`, `Tutorial`, `Comparison`, `What Is`) + topic tags (your tool/category names).\r
\r
### 7f. Build the metadata bundle\r
\r
Write the per-post fields into `tmp/blog-drafts/\x3Cslug>.params.json`, then run the\r
builder. It validates required fields and maps the status flags to every adapter.\r
\r
`params.json` shape:\r
\r
```json\r
{\r
  "title": "\x3Cheadline>",\r
  "tags": ["How To", "n8n"],\r
  "author": {"slug": "\x3Cauthor-slug>", "name": "\x3Cauthor display name>"},\r
  "meta_title": "\x3CSEO title under 60 chars>",\r
  "meta_description": "\x3CSEO description, 140-160 chars>",\r
  "custom_excerpt": "\x3Cdek shown on index page>",\r
  "feature_image": "",\r
  "feature_image_alt": "",\r
  "feature_image_caption": "",\r
  "publish": false,\r
  "publish_at": null\r
}\r
```\r
\r
First tag is the primary tag (passed to 7b for the breadcrumb). Set `publish: true`\r
for `--publish`; `publish_at` (ISO-UTC) for `--publish-at` (mutually exclusive).\r
\r
```bash\r
python3 scripts/seo-blog-writer/build-metadata.py "\x3Cslug>"\r
```\r
\r
### 7g. Pre-publish bundle validation\r
\r
Before invoking the platform adapter, all of these must hold:\r
\r
```bash\r
python3 scripts/seo-blog-writer/validate-bundle.py "\x3Cslug>"\r
```\r
\r
If any assert fires, fix and re-build before Step 8.\r
\r
### 7h. Refresh metadata snapshot\r
\r
Save a small JSON snapshot of the post's facts so a future refresh pass can identify staleness without re-reading the prose. Cheap to write now; expensive to backfill at 500 posts.\r
\r
```bash\r
python3 scripts/seo-blog-writer/refresh-meta.py "\x3Cslug>" "\x3Cformat>"\r
```\r
\r
When a topic refresh comes due (typically every 6-12 months for high-traffic posts), the refresh skill (future / your-own) diffs the snapshot's `versions_cited` against current vendor docs. Versions that have rolled forward by a major release are flagged for rewrite; everything else is left alone.\r
\r
### 7i. Glossary auto-link (optional)\r
\r
If you maintain a glossary of technical terms with definition pages on your site, pipe the draft HTML through `scripts/inject-glossary-links.py` to turn the first mention of each known term into an internal link to its definition page. Each link also carries a `data-definition` attribute that the bundled `references/decorate.js` snippet renders as a hover tooltip on the published page.\r
\r
**Skip this step if** you don't have a `glossary.json` file yet — there's no default. See [references/glossary-schema.md](references/glossary-schema.md) for the file shape and a starter example.\r
\r
```bash\r
python3 scripts/inject-glossary-links.py \\r
    tmp/blog-drafts/\x3Cslug>.draft.html \\r
    --glossary path/to/glossary.json \\r
    --base-url /glossary/ \\r
    --max-links 6 \\r
    > tmp/blog-drafts/\x3Cslug>.draft.linked.html\r
\r
mv tmp/blog-drafts/\x3Cslug>.draft.linked.html tmp/blog-drafts/\x3Cslug>.draft.html\r
```\r
\r
The injector:\r
\r
- Links **first occurrence only** per term per post (Wikipedia rule).\r
- Caps at `--max-links` (default 6), priority-sorted from the glossary.\r
- Skips headings, code/pre, tables, blockquotes, asides, existing links, and the TL;DR paragraph.\r
- Rejects matches embedded in identifier-like compounds (`user-agent` won't match `agent`, `@scope/ai-seo-mcp` won't match `mcp`).\r
- Writes a `data-definition` attribute on each link for the tooltip.\r
\r
Run order: **after Step 7g validates the draft** so the validator's structural asserts run on clean HTML; **before Step 8 publishes** so the linked HTML is what ships. Glossary links count as internal navigation, not outbound — the Step 7g outbound-survival assert ignores them.\r
\r
To enable the hover tooltip on the live site, copy `skills/seo-blog-writer/references/decorate.js` into your theme bundle (or paste it inline in a `\x3Cscript>` tag in your site `\x3Chead>`) once. It's self-contained, ~1 KB, no dependencies, and skips itself on `/glossary/*` pages.\r
\r
---\r
\r
## Step 8 — Publish via the platform adapter\r
\r
Pick one adapter per run. Each adapter reads the same bundle (`\x3Cslug>.draft.html`, `\x3Cslug>.schema.html`, `\x3Cslug>.metadata.json`) and writes the post to its target platform.\r
\r
---\r
\r
### Adapter A — Ghost (Admin API)\r
\r
The Ghost adapter uses the Admin API over HTTPS. No Docker, no SSH — just authenticated POST to `/ghost/api/admin/posts/`.\r
\r
**Credentials**:\r
\r
| Env var | Source | Shape |\r
|---|---|---|\r
| `GHOST_URL` | Your Ghost site URL | `https://blog.example.com` (no trailing slash) |\r
| `GHOST_ADMIN_KEY` | Ghost admin -> Settings -> Integrations -> (your integration) -> **Admin API Key** | `\x3C24-hex>:\x3C64-hex>` combined |\r
\r
Preflight:\r
\r
```bash\r
curl -sS "$GHOST_URL/ghost/api/admin/site/" | head -c 80\r
[ -n "$GHOST_URL" ] && [ -n "$GHOST_ADMIN_KEY" ] && echo "keys present" || echo "MISSING"\r
```\r
\r
**Image upload** (call once per figure, then splice the returned URL into the draft):\r
\r
```bash\r
python3 scripts/seo-blog-writer/ghost-upload-image.py "\x3Cimage-path>"\r
```\r
\r
**Publish the post**:\r
\r
```bash\r
python3 scripts/seo-blog-writer/publish-ghost.py "\x3Cslug>"\r
```\r
\r
`?source=html` tells Ghost to convert the `html` field into Lexical. Without it, Ghost treats the field as Lexical JSON and the POST fails with a 422.\r
\r
**Python deps**: `pip install requests pyjwt`. PyJWT 2.x required.\r
\r
---\r
\r
### Adapter B — WordPress (REST API)\r
\r
Uses the WordPress REST API with **Application Password** auth (Users -> Profile -> Application Passwords). Works on any WP site with REST exposed at `/wp-json/wp/v2/`.\r
\r
**Credentials**:\r
\r
| Env var | Source | Shape |\r
|---|---|---|\r
| `WP_URL` | Your WordPress site URL | `https://blog.example.com` (no trailing slash) |\r
| `WP_USER` | The WP username the app password belongs to | `admin` |\r
| `WP_APP_PASSWORD` | Profile -> Application Passwords -> new -> "seo-blog-writer" | `xxxx xxxx xxxx xxxx xxxx xxxx` |\r
\r
Preflight:\r
\r
```bash\r
curl -sS "$WP_URL/wp-json/wp/v2/" | head -c 120\r
[ -n "$WP_URL" ] && [ -n "$WP_USER" ] && [ -n "$WP_APP_PASSWORD" ] && echo "keys present" || echo "MISSING"\r
```\r
\r
**Image upload** (returns the media id and URL):\r
\r
```bash\r
python3 scripts/seo-blog-writer/wp-upload-image.py "\x3Cimage-path>"\r
```\r
\r
**Publish the post**:\r
\r
```bash\r
python3 scripts/seo-blog-writer/publish-wordpress.py "\x3Cslug>"\r
```\r
\r
**Notes**:\r
- `featured_media` in the post payload is a media **id**, not a URL. Upload the feature image first, capture the id, then set `post["featured_media"] = \x3Cid>`.\r
- WordPress accepts `\x3Cscript>` in `content` only if the user has the `unfiltered_html` capability (admins do by default; editors may not). If your user lacks it, install a small theme snippet that reads the schema from a post meta key into `wp_head`.\r
\r
---\r
\r
### Adapter C — Static-site (file output)\r
\r
For Hugo / Astro / Eleventy / Jekyll / Next-MDX style setups where posts live as files in a git repo. The adapter writes the bundle into the target directory; your usual build + deploy takes it from there.\r
\r
**No credentials.** Just a target path.\r
\r
```bash\r
python3 scripts/seo-blog-writer/publish-static.py "\x3Cslug>" "\x3Cout-dir>"\r
```\r
\r
Your SSG's layout template needs one line to include the schema in `\x3Chead>` — e.g. for Hugo:\r
\r
```html\r
{{ if (fileExists (printf "content/posts/%s.schema.html" .File.BaseFileName)) }}\r
  {{ readFile (printf "content/posts/%s.schema.html" .File.BaseFileName) | safeHTML }}\r
{{ end }}\r
```\r
\r
For Astro / Eleventy / Next, do the equivalent (read file at build time, inject into the layout head).\r
\r
---\r
\r
### Adapter D — bring-your-own\r
\r
The bundle is a stable contract. Any platform with an "upload an image" and a "create a post" endpoint can be adapted in ~50 lines. The contract:\r
\r
- `\x3Cslug>.draft.html` — body HTML, post-scrub, post-audit.\r
- `\x3Cslug>.schema.html` — JSON-LD `\x3Cscript>` blocks to inject in `\x3Chead>`.\r
- `\x3Cslug>.metadata.json` — title, slug, tags (string list), author (slug + name), meta title/desc, excerpt, feature image (URL or local path), status (`draft` / `published` / `scheduled`), published_at (ISO).\r
\r
Adapter examples shipped above (Ghost, WordPress, static) cover ~90% of small-publisher use cases. Webflow CMS, Sanity, Strapi, and Contentful each take a similar shape: POST to the platform's content endpoint with their auth header, body field, and metadata fields.\r
\r
---\r
\r
### Step 8b. Report back to the user\r
\r
Whatever adapter ran, the final report includes:\r
\r
- Draft URL or live URL (`\x3Cbase-url>/\x3Cslug>/` if published; admin edit URL if draft).\r
- Platform admin / repo edit URL.\r
- Word count, tag list, author slug.\r
- Confirmation: scrub passed, AI-SEO audit applied, FAQ block present, JSON-LD injected.\r
- Figure URLs and captions.\r
\r
---\r
\r
## Step 9 — Verify live post (only if `--publish`)\r
\r
```bash\r
# Post is reachable\r
curl -sSI "\x3Cbase-url>/\x3Cslug>/" | head -5\r
\r
# Post in RSS\r
curl -sS "\x3Cbase-url>/rss/" | grep -o "\x3Ctitle>[^\x3C]*\x3C/title>" | head -5\r
\r
# Post in sitemap (path varies by platform — Ghost: /sitemap-posts.xml; WP: /sitemap.xml; SSG: as configured)\r
curl -sS "\x3Cbase-url>/sitemap-posts.xml" | grep "\x3Cslug>"\r
\r
# OG + full schema set rendered\r
curl -sS "\x3Cbase-url>/\x3Cslug>/" | grep -o 'property="og:[^"]*"' | sort -u\r
curl -sS "\x3Cbase-url>/\x3Cslug>/" | grep -oE '"@type":\s*"[^"]+"' | sort -u\r
```\r
\r
**Expected:** `HTTP/2 200`, slug in RSS and sitemap, `og:title`/`og:description` present. The `"@type"` set must include **`Article`** (or `BlogPosting`), **`FAQPage`**, and **`BreadcrumbList`**; procedural how-to posts must also include **`HowTo`**. Missing FAQPage/BreadcrumbList means the schema slot wasn't wired correctly — check the platform-specific head-injection field.\r
\r
---\r
\r
## What this skill does NOT do\r
\r
- **Does not commit to git.** Adapters write to CMS APIs or to your static-site directory; the latter you commit yourself.\r
- **Does not schedule posts by default.** Pass `--publish-at \x3CISO-UTC>` to schedule. Without it the post lands as draft (default) or live (`--publish`).\r
- **Does not handle member-only posts, newsletters, or email sends.** Each platform's newsletter flow is manual via its admin UI.\r
- **Does not generate figures.** Use the companion `blog-figure-svg` skill for SVG charts, taxonomies, and flow diagrams.\r
- **Does not research topics from scratch.** Use the companion `blog-topic-research` skill to validate a topic has real demand signals before drafting.\r
\r
---\r
\r
## Failure modes\r
\r
| Symptom | Adapter | Cause | Fix |\r
|---|---|---|---|\r
| `401 Unauthorized` | Ghost / WordPress | Key expired / wrong key / wrong app-password | Regenerate the integration / app password |\r
| Ghost `422 Validation failed: Value in [posts.html] cannot be blank` | Ghost | Missing `?source=html` | Add the query param |\r
| Ghost `422` with `feature_image_alt` in message | Ghost | Alt text >191 chars | Trim to \x3C=191; Step 7g asserts this |\r
| `404` on slug after publish | any | Post saved as draft (default) | Drafts only reachable via admin editor URL |\r
| Body shows as one HTML blob | Ghost | Ghost fell back to plain-text mode | Re-post with `?source=html` |\r
| Smart quotes reappear in rendered post | Ghost | Ghost typographer auto-conversion | Settings -> Publication: turn off "Use typographer's quotes" |\r
| Wrong slug | any | Platform auto-slugged from title | PUT/PATCH the post with the corrected slug |\r
| Ghost `409 Conflict` on PUT | Ghost | Stale `updated_at` | Re-GET to refresh, retry |\r
| Author silently substituted | Ghost / WordPress | Author slug doesn't exist / user lacks `publish_posts` | Create the user; PUT correction with correct slug or user id |\r
| Live page missing FAQPage / HowTo `@type` (Step 9) | Ghost | JSON-LD was inlined in the body and stripped by Lexical conversion | PUT with `codeinjection_head` set to `\x3Cslug>.schema.html`; echo current `updated_at` to avoid 409 |\r
| WordPress strips `\x3Cscript type="application/ld+json">` from body | WordPress | User lacks `unfiltered_html` | Move schema injection to a theme hook reading a post meta key |\r
\r
---\r
\r
## Companion skills\r
\r
- **`blog-topic-research`** — validate a long-tail topic has real demand signals (PAA, Reddit threads, GitHub issues) before drafting. Run this *before* this skill.\r
- **`blog-figure-svg`** — generate accessible SVG figures (flow diagrams, comparison charts, taxonomy diagrams) with consistent styling. Run this *during Step 6* if the post needs illustrations.\r
\r
Together, the three form a complete long-tail SEO publishing pipeline: research the topic, write the post, illustrate it, publish.\r
\r
---\r
\r
## Maintenance scripts\r
\r
The per-post scrub in Step 4a covers the common LLM-tell characters and the per-post audit in Step 7g enforces the structural rules. For corpus-wide drift — characters or banlist phrases that crept back in across many posts — there's a separate audit script in the repo:\r
\r
```bash\r
# Sweep your published-content directory for non-ASCII chars + prose banlist\r
python3 scripts/audit-corpus.py path/to/your/content/\r
\r
# Examples (per platform):\r
python3 scripts/audit-corpus.py tmp/blog-drafts/                  # current drafts\r
python3 scripts/audit-corpus.py content/posts/                    # Hugo / Astro / 11ty\r
python3 scripts/audit-corpus.py site/source/_posts/               # Jekyll\r
\r
# Add domain-specific terms you want flagged (comma-separated):\r
python3 scripts/audit-corpus.py content/posts/ --extra "synergy,best-in-class"\r
\r
# CI mode: exit 1 on any hit, pipe to your notifier or fail the build\r
python3 scripts/audit-corpus.py content/posts/ >/dev/null || echo "drift detected"\r
```\r
\r
Default scan covers `*.html` and `*.md`. The script exits `0` clean / `1` on hits / `2` on bad invocation, so it composes with CI. Run it weekly (or as a pre-deploy step) — much cheaper than re-reading every post by hand.\r
\r
Don't point it at the publishing-skills repo itself or at the seo-blog-writer SKILL.md: both contain the banlist literals as data and will self-flag. Target your *content* directory, not your *tooling* directory.\r
安全使用建议
Install this only for agents you trust to draft and create Ghost posts. Configure a least-privilege Ghost integration if possible, keep GHOST_ADMIN_KEY out of git, review generated drafts before using --publish or --publish-at, and periodically clean tmp/blog-drafts if the drafts may contain private material.
能力标签
requires-sensitive-credentials
能力评估
Purpose & Capability
The skill clearly describes an end-to-end blog workflow: research, draft, scrub, SEO audit, and publish or schedule posts to a Ghost CMS site via the Admin API.
Instruction Scope
It can perform external publishing and scheduling, but the artifact states that draft is the default and live publication requires --publish or --publish-at.
Install Mechanism
The metadata only declares a python3 binary requirement; no hidden installer, package bootstrap, or automatic persistence mechanism is shown.
Credentials
It requires GHOST_URL and GHOST_ADMIN_KEY and uses network access to Ghost and research sources; that is proportionate for managing Ghost posts, though users should treat the admin key as sensitive.
Persistence & Privilege
Local writes are limited to tmp/blog-drafts artifacts, and the skill explicitly says it does not commit to git or schedule by default.
如何使用
  1. 确保已安装 OpenClaw(本地或 Docker 部署)
  2. 在对话框中输入安装命令:/install automatelab-seo-blog-writer
  3. 安装完成后,直接呼叫该 Skill 的名称或使用 /automatelab-seo-blog-writer 触发
  4. 根据 Skill 的参数说明提供必要输入,即可获得结构化输出
版本历史
v2.2.0
Initial publish (slug automatelab-seo-blog-writer; seo-blog-writer taken by unrelated skill)
元数据
Slug automatelab-seo-blog-writer
版本 2.2.0
许可证 MIT-0
累计安装 0
当前安装数 0
历史版本数 1
常见问题

Seo Blog Writer 是什么?

Turn a single long-tail query into a publish-ready blog post that ranks in search and gets quoted by AI assistants. Runs the full pipeline: classify the topi... 它是一个面向 Claude Code / OpenClaw 的 AI Agent Skill 插件,目前累计下载 51 次。

如何安装 Seo Blog Writer?

在 OpenClaw 或 Claude Code 对话框中运行命令「/install automatelab-seo-blog-writer」即可一键安装,无需额外配置。

Seo Blog Writer 是免费的吗?

是的,Seo Blog Writer 完全免费,采用 MIT-0 许可证,可自由下载、安装和使用。

Seo Blog Writer 支持哪些平台?

Seo Blog Writer 跨平台运行,可在任意部署了 OpenClaw / Claude Code 的环境中使用(cross-platform)。

谁开发了 Seo Blog Writer?

由 AutomateLab(@automatelab)开发并维护,当前版本 v2.2.0。

💬 留言讨论