Use this skill whenever the user wants to publish, update, or batch-upload blog content to a WordPress site via the REST API. Handles auth, markdown conversi...
---
name: WordPress Blog Publisher
description: Use this skill whenever the user wants to publish, update, or batch-upload blog content to a WordPress site via the REST API. Handles auth, markdown conversion, image uploads, scheduling, and writing permalink back to upstream systems.
---
# WordPress Blog Publisher
This skill turns an AI agent into a reliable publisher for WordPress sites via the REST API (`/wp-json/wp/v2/*`). Designed for batch SEO content workflows, it handles authentication, markdown-to-HTML conversion, media uploads, post scheduling, and status writeback to upstream systems like Airtable, Bitable, or Google Sheets.
## Quick Reference
| Decision | Strong Choice | Acceptable | Weak / Avoid |
|----------|---------------|------------||--------------|
| Auth method | Application Password (HTTP Basic) | JWT token | Login password (never) |
| Content format | Markdown → HTML (convert first) | Raw HTML | Raw markdown (WP won't render it) |
| Batch approach | Dry-run first post, confirm, then batch | Process all with logs | Process all without validation |
| Image handling | Upload to WP media library, rewrite src | External CDN links | Local file paths |
| Post status | `publish` or `future` (scheduled) | `draft` for review | `private` (unless intentional) |
| Error handling | Log and continue on non-critical errors | Stop on all errors | Silent fail |
| Permalink writeback | Write to upstream system after publish | Log to file | No writeback |
## Problems This Skill Solves
1. **Manual copy-paste publishing bottleneck** — writing and publishing blog posts one-by-one when a batch of 20–200 SEO articles needs to go live.
2. **Markdown format mismatch** — AI-generated content is usually markdown, but WP's classic editor expects HTML and the block editor has its own format requirements.
3. **Image scatter** — inline images in markdown that point to external URLs or local paths need to be uploaded to the WP media library and src-rewritten.
4. **No upstream status tracking** — after publishing, many workflows lose track of which posts are live vs. draft vs. scheduled.
5. **Auth confusion** — using login passwords (wrong) vs. Application Passwords (correct) is the #1 cause of 401 errors.
6. **Category/tag ID mismatch** — WP uses numeric IDs for categories and tags, not names; this skill handles the lookup.
7. **Scheduling complexity** — batch content often needs to be spread across a publication calendar rather than published all at once.
## Workflow
### Step 1 — Authenticate
**Required credentials**:
- `site_url`: e.g., `https://example.com` (no trailing slash)
- `username`: WP admin username
- `app_password`: Application Password from WP Admin → Users → Profile → Application Passwords
**Auth header**:
```
Authorization: Basic base64(username:app_password)
```
**Test connection**:
```
GET {site_url}/wp-json/wp/v2/users/me
```
If 200, auth is working. If 401, check Application Password setup. If 403, user lacks `publish_posts` capability.
### Step 2 — Prepare Content
- Strip YAML front-matter; use `title`, `slug`, `categories`, `tags` fields from it
- Convert markdown to HTML using a proper converter (not regex substitution)
- Preserve: code fences (`<pre><code>`), tables, nested lists
- Remove: `# H1` title (use as post title, not body content)
### Step 3 — Resolve Category and Tag IDs
WP requires numeric IDs, not names:
```
GET {site_url}/wp-json/wp/v2/categories?search=seo&per_page=5
```
Cache the ID map for the batch to avoid repeated lookups.
### Step 4 — Upload Media (Featured Image)
```
POST {site_url}/wp-json/wp/v2/media
Headers:
Content-Disposition: attachment; filename="image.jpg"
Content-Type: image/jpeg
Body: [binary image data]
```
### Step 5 — Create or Update Post
```json
POST {site_url}/wp-json/wp/v2/posts
{
"title": "Your Post Title",
"content": "<p>HTML content here</p>",
"slug": "your-post-slug",
"status": "publish",
"categories": [12, 34],
"tags": [56, 78],
"featured_media": 1234,
"date_gmt": "2025-06-15T09:00:00"
}
```
Response includes `"link"` — the live permalink. Save this.
### Step 6 — Write Status Back to Upstream System
After each successful publish, write permalink and timestamp back:
- **Airtable**: PATCH record with `permalink` and `wp_status` fields
- **Google Sheets**: Update row via Sheets API
- **Bitable**: Update record via Lark Open API
- **Local log**: Append `{slug, wp_post_id, permalink, published_at}` to CSV/JSON
### Step 7 — Batch Mode Protocol
1. Process first post fully, show draft URL
2. Wait for confirmation
3. Process remaining with 1–2 second delay between requests
4. Show running status: `[3/20] Published: /seo-guide-2025 ✓`
5. On failure: log error, continue with next post
6. Final summary: `20 published, 1 failed (see log)`
## Worked Examples
### Example 1 — Batch Publish 10 SEO Articles from Airtable
1. Fetch rows where `Status = "Ready to Publish"` from Airtable
2. For each row: convert markdown → HTML, upload featured image, resolve category ID
3. Create WP post with `status: "future"` and `date_gmt` staggered by 1 day each
4. Write permalink + WP post ID back to Airtable
### Example 2 — Update Existing Posts with New Content
1. For each slug: `GET /wp-json/wp/v2/posts?slug={slug}&status=any` to find post ID
2. Convert updated markdown to HTML
3. `POST /wp-json/wp/v2/posts/{id}` with updated content
## Common Mistakes
1. **Using login password** — results in 401; use Application Password.
2. **Posting raw markdown** — always convert to HTML first.
3. **Wrong category IDs** — always look up IDs via the API.
4. **Images > 2MB** — resize before uploading.
5. **Using `date` instead of `date_gmt`** — always use UTC.
6. **No dry-run on batches** — validate one post before running 50.
7. **Not writing permalink back** — without writeback, you lose track of published posts.
8. **Silent failures in batch** — always log errors explicitly.
## Resources
- `references/api-reference.md` — WP REST API endpoints quick reference
- `references/markdown-to-html-rules.md` — Conversion rules and edge cases
- `assets/wp-publish-checklist.md` — Pre-publish and post-publish quality checklist
don't have the plugin yet? install it then click "run inline in claude" again.
added structured inputs section with env var names and oauth scopes, expanded procedure into 7 distinct numbered steps with explicit inputs/outputs for each, extracted decision points into dedicated section with if-else branches for auth failures/rate limits/missing categories/batch cancellation, formalized output contract with http status codes and log file formats, defined outcome signals so users know when skill succeeded.
use this skill to publish, update, or batch-upload blog posts to a wordpress site via the rest api (/wp-json/wp/v2/*). handles application password auth, markdown-to-html conversion, media uploads to the wp library, post scheduling across a calendar, and status writeback to upstream systems (airtable, bitable, google sheets). designed for workflows where 20-200 seo articles need to go live on a schedule without manual copy-paste per post.
wordpress site credentials (required):
site_url: base domain, no trailing slash (e.g., https://example.com)username: wp admin usernameapp_password: application password from wp admin > users > [your profile] > application passwords section. never use login password.content inputs (required):
title, slug, content (markdown), categories (comma-separated names or array of ids), tags (comma-separated names or array of ids), optional featured_image_url or featured_image_fileupstream system connection (optional but recommended):
AIRTABLE_API_KEY), and field names for permalink, wp_status, wp_post_idGOOGLE_SHEETS_CREDENTIALS), columns for permalink and publish timestampLARK_APP_TOKEN), table id, record id fieldexternal connections (optional):
authorization: basic [base64(username:app_password)]get {site_url}/wp-json/wp/v2/users/me with auth headerpublish_posts capability in wp admininputs: site_url, username, app_password outputs: valid auth header, user object with capabilities confirmed
title, slug, categories, tags, publish_date (optional), featured_image_url (optional)# Title) from markdown body (this becomes post title, not body content)<pre><code>, tables as <table>, nested lists as nested <ul>/<ol>, blockquotes as <blockquote><img src="...">) and note all image urls for later uploadinputs: markdown content with optional front-matter outputs: post title (string), post slug (string), post content (valid html), list of image urls, metadata dict with categories, tags, publish_date
get {site_url}/wp-json/wp/v2/categories?search={category_name}&per_page=5 with auth headerid field from matching category/wp-json/wp/v2/tags endpointinputs: category names list, tag names list, site_url, auth header outputs: dict mapping {category_name: id}, dict mapping {tag_name: id}
post {site_url}/wp-json/wp/v2/media with headers: content-disposition: attachment; filename="{filename}", content-type: image/jpeg (or appropriate type), body as binary image data, auth headerpost {site_url}/wp-json/wp/v2/media with same headers and authinputs: featured_image url or file, list of inline image urls, post content html, site_url, auth header outputs: featured_media_id (numeric), updated post content html with rewritten image src attributes
get {site_url}/wp-json/wp/v2/posts?slug={slug}&status=any with auth header; extract post id from responsetitle: post title (string)content: post content (valid html from step 2)slug: post slug (string)status: "publish" for immediate publish, "future" for scheduled, or "draft" for reviewcategories: array of numeric category ids from step 3tags: array of numeric tag ids from step 3featured_media: numeric media id from step 4 (if featured image exists)date_gmt: utc datetime string for publication (e.g., "2025-06-15T09:00:00"); use this field, never date (local timezone)post {site_url}/wp-json/wp/v2/posts with json payload and auth headerpost {site_url}/wp-json/wp/v2/posts/{post_id} with json payload and auth headerid (post id), link (permalink), date_gmt (published datetime)inputs: post title, slug, html content, category ids, tag ids, featured media id, publish status, publish datetime (utc), wp_post_id (if update), site_url, auth header outputs: wp_post_id (numeric), permalink (url string), published_datetime (utc string)
patch https://api.airtable.com/v0/{base_id}/{table_name}/{record_id} with auth authorization: bearer {airtable_api_key} and json payload: {"fields": {"permalink": "{permalink}", "wp_status": "published", "wp_post_id": {wp_post_id}, "published_at": "{published_datetime}"}}put https://sheets.googleapis.com/v4/spreadsheets/{sheet_id}/values/{range}!A1 with values array containing permalink, post id, status, timestamppatch https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records/{record_id} with auth authorization: bearer {access_token} and json payload with field updates{"slug": "{slug}", "wp_post_id": {wp_post_id}, "permalink": "{permalink}", "published_at": "{published_datetime}"} to jsonl log file, or csv row to csv log fileinputs: wp_post_id, permalink, published_datetime, upstream_system_type, upstream credentials (airtable key, google sheets credentials, bitable token, or log file path) outputs: writeback success status (true/false), upstream record id or row updated count
[{current}/{total}] published: {slug} {permalink} ✓ or [{current}/{total}] failed: {slug} , {error_reason}{count_published} published, {count_failed} failed, {count_skipped} skipped. see log: {log_file_path}inputs: list of posts (2+), dry_run_mode (true for batch), user_confirmation, upstream_system_type outputs: batch summary dict with counts, completion status, log file path
if user provides markdown content then convert to html using step 2 procedure. else if user provides raw html then skip conversion; validate html is well-formed before posting. else reject input and ask for markdown or html.
if featured image url provided then download and upload to wp media library via step 4. else if featured_image_file path provided then read local file and upload via step 4. else proceed without featured image (featured_media field omitted from post payload).
if category or tag name does not exist in wordpress then log warning "{category_name} not found, skipping for this post". continue post creation without that category or tag.
if image file size > 2mb then compress/resize image before upload. else upload as-is.
if api response status is 401 (unauthorized) then stop batch immediately. log "auth failed: check application password and site url". do not attempt retry.
if api response status is 429 (rate limit) then pause for 60 seconds. retry the request once. if retry fails, log error and continue to next post.
if post status is "future" (scheduled) and publish datetime is in the past then change status to "publish" and post immediately. log warning "scheduled date is past, publishing immediately".
if batch mode is enabled (2+ posts) and no user confirmation after dry-run then abort batch. delete draft post created in dry-run (optional). log "batch cancelled by user".
if upstream writeback fails (e.g., airtable api error) then log error with response status and body. continue to next post. do not halt batch.
if post update (not create) and post id lookup returns 0 results then treat as new post and create instead. log "post slug not found; creating as new post".
on successful post creation:
id (numeric), link (permalink string), date_gmt (utc datetime string), slug (string){"id": 1234, "slug": "seo-guide-2025", "link": "https://example.com/seo-guide-2025/", "date_gmt": "2025-06-15T09:00:00"}on successful post update:
id (numeric), link (permalink string), modified_gmt (utc datetime string)on successful writeback to upstream:
batch completion log file:
{working_dir}/wp-publish-log-{timestamp}.jsonl or .csvslug, wp_post_id, permalink, published_at, status (success/failed), error_message (if failed)slug,wp_post_id,permalink,published_at,status,error_message{"slug":"seo-guide-2025","wp_post_id":1234,"permalink":"https://example.com/seo-guide-2025/","published_at":"2025-06-15T09:00:00","status":"success"}error log file:
{working_dir}/wp-publish-errors-{timestamp}.log2025-06-15T09:00:15Z | seo-guide-2025 | 400 | {"code":"rest_invalid_param","message":"Invalid parameter(s): categories"}user knows the skill worked when:
post is live or scheduled: permalink is accessible at expected url (e.g., https://example.com/seo-guide-2025/) and post content appears with correct title, html-converted body, featured image, categories, and tags. if status is "future", post is not yet visible but appears in wp admin with scheduled datetime.
images are in wp library: featured image and all inline images are uploaded to wp media library (wp admin > media library lists them) and src attributes in post body point to wp media urls (not external urls or local paths).
upstream record is updated: airtable/sheets/bitable record or log file contains permalink, wp post id, and publish timestamp. user can click permalink in upstream system and land on live post.
batch summary is printed: for batches, stdout shows final count: "{N} published, {M} failed". user can open log file and see each post's status and any error details.
no silent failures: every post attempt is logged (success or failure); user never publishes without knowing whether each post succeeded.
dry-run works: first post in batch is created as draft with correct title, content, and formatting. user sees draft url in output and can review before confirming batch.
credits: original skill by leooooooow (clawhub).