- Go 60.1%
- PowerShell 39.9%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| cmd/wpost | ||
| internal | ||
| prompts | ||
| scripts | ||
| .gitignore | ||
| go.mod | ||
| go.sum | ||
| README.md | ||
wp-claude-post
Automated WordPress blogging: the Claude CLI writes the article, a small Go binary publishes it through the WordPress REST API, and a PowerShell script ties the two together so Task Scheduler can run it unattended.
prompts/post-prompt.txt your prompt, with placeholders
|
v
scripts/New-BlogPost.ps1 ---> claude -p ---> output/20260820-143000-my-title.md
|
v
bin/wpost.exe ---> POST https://your-site/wp-json/wp/v2/posts
The two halves are independent: wpost.exe publishes any article file you give
it, whether Claude wrote it or you did.
Requirements
Go 1.21+ to build the poster, PowerShell 7 (pwsh) to run the scripts, and the
Claude CLI on PATH and already signed in — run claude once interactively if
you have not.
Setup
1. Build the poster
go build -o bin\wpost.exe .\cmd\wpost
2. Create a WordPress application password
In WordPress: Users → Profile → Application Passwords, add one named
wp-claude-post, and copy the generated value (it looks like
abcd efgh ijkl mnop qrst uvwx). This is not your login password, and it can
be revoked on its own.
The account needs the Author role to create posts, or Editor to publish them.
Store the credentials as user environment variables so they never appear in a command line, a script, or the scheduled task definition:
[Environment]::SetEnvironmentVariable('WP_URL', 'https://example.com', 'User')
[Environment]::SetEnvironmentVariable('WP_USER', 'editor', 'User')
[Environment]::SetEnvironmentVariable('WP_APP_PASSWORD', 'abcd efgh ijkl mnop qrst uvwx', 'User')
Open a new shell afterwards, then verify:
.\bin\wpost.exe -check
3. Write your prompt
Edit prompts/post-prompt.txt. It is a plain text file; the script substitutes
these placeholders before sending it to Claude:
| Placeholder | Replaced with |
|---|---|
{{TOPIC}} |
the next topic from -TopicsFile (empty when unused) |
{{DATE}} |
current date, yyyy-MM-dd |
{{TIME}} |
current time, HH:mm |
{{INDEX}} / {{COUNT}} |
article number within the run / total requested |
{{RECENT_TITLES}} |
recently published titles, so Claude avoids repeats |
The shipped prompt asks for front matter followed by a Markdown body. Keep that part if you change the rest — it is what gives posts their title, slug, excerpt, categories, and tags.
Optionally copy prompts/topics.example.txt to prompts/topics.txt and list
your subjects, one per line. The script walks the list in order and records what
it used in logs/topics-used.txt, so two articles on the same day differ.
Usage
Generate without touching the site — always start here:
.\scripts\New-BlogPost.ps1 -Count 2 -GenerateOnly
Read what landed in output\, then publish as drafts:
.\scripts\New-BlogPost.ps1 -Count 2 -Status draft
Once you trust the prompt, publish live:
.\scripts\New-BlogPost.ps1 -Count 2 -Status publish -Categories 'Engineering' -Tags 'go, automation'
Publish a file you already have:
.\bin\wpost.exe -file output\20260820-143000-my-title.md -status publish
New-BlogPost.ps1 parameters
| Parameter | Default | Purpose |
|---|---|---|
-Count |
1 |
Articles to produce in this run |
-GenerateOnly |
off | Write the files, publish nothing |
-DryRun |
off | Run the poster in dry-run: parses and reports, sends nothing |
-PromptFile |
prompts\post-prompt.txt |
Prompt template |
-TopicsFile |
none | Topic list to rotate through |
-Status |
draft |
draft, publish, pending or private |
-Categories / -Tags |
none | Added to whatever the article's front matter declares |
-Model |
sonnet |
Model alias passed to claude --model |
-MinWords |
150 |
Replies shorter than this are rejected, not published |
-TimeoutSeconds |
900 |
Hard limit on one generation |
-ClaudeExtraArgs |
none | Extra CLI flags, e.g. @('--allowedTools','WebSearch') |
-AppPasswordFile |
none | Read the application password from a file instead of a flag |
-OutputDir / -LogDir |
output\ / logs\ |
Where articles and logs go |
-GenerateOnly skips the poster entirely; -DryRun still runs it, which is the
better check when you want to confirm the article parses correctly.
wpost flags
Run .\bin\wpost.exe -h for the full list. The ones that matter:
| Flag | Default | Purpose |
|---|---|---|
-url, -user, -password |
WP_URL, WP_USER, WP_APP_PASSWORD |
Site and credentials |
-file |
required | Article file to publish |
-status |
draft |
Post status |
-title, -slug, -excerpt |
from the file | Override the front matter |
-categories, -tags |
none | Names or IDs, comma separated; names are created if missing |
-date |
none | "2026-08-21 09:00" or RFC3339; a future date schedules the post |
-format |
auto |
markdown, html, or sniff |
-excerpt-words |
40 |
Words used for an excerpt when the article has none |
-create-terms |
true |
Set false to skip unknown categories and tags |
-check |
off | Verify credentials and exit |
-dry-run |
off | Report what would be posted, send nothing |
-json |
off | Print the result as JSON on stdout |
-verbose |
off | Log every API request to stderr |
-rest-route |
off | Use ?rest_route= endpoints (no pretty permalinks) |
-insecure |
off | Skip TLS verification, for local dev sites only |
Exit codes: 0 success, 1 API or network failure, 2 bad usage, 3
authentication or permission failure, 4 unusable article file.
Article file format
Front matter is optional but recommended; command line flags override it.
---
title: How Caching Changes a Slow API Endpoint
slug: caching-slow-api-endpoint
excerpt: What actually happens to latency when you put a cache in front.
categories: Engineering, Performance
tags: caching, api, performance
---
The opening paragraph, without repeating the title as a heading.
## First section
Markdown body: headings, lists, **bold**, [links](https://example.com), tables,
and fenced code blocks are all converted to HTML before posting.
Without front matter the first # Heading becomes the title and is removed from
the body, so your theme does not render it twice. A reply wrapped in a single
code fence is unwrapped automatically. Raw HTML — including Gutenberg block
comments — passes through untouched.
Running it twice a day
.\scripts\Register-BlogTask.ps1 -Times '08:00','17:00' -Status draft
That registers one scheduled task with two daily triggers, each producing a single article. Useful variations:
# Rotate through your topic list and file everything as drafts
.\scripts\Register-BlogTask.ps1 -Times '08:00','17:00' -TopicsFile 'C:\projects\wp-claude-post\prompts\topics.txt'
# Generate only, so you publish by hand
.\scripts\Register-BlogTask.ps1 -GenerateOnly
# Remove it
.\scripts\Register-BlogTask.ps1 -Unregister
The task runs as your user with "run only when logged on", which is what lets the Claude CLI reuse the credentials in your profile. Credentials for WordPress come from the user environment variables, not from the task definition.
Test it without waiting for the trigger:
Start-ScheduledTask -TaskName 'WP Claude Post - daily articles'
Get-Content .\logs\run-*.log -Tail 20
Start with -Status draft for a week before switching to publish. Nothing
here reviews the article for accuracy — that part is still yours.
What gets written where
| Path | Contents |
|---|---|
output\ |
Generated articles, yyyyMMdd-HHmmss-slug.md |
output\rejected\ |
Replies that failed validation, kept for diagnosis |
logs\run-YYYY-MM-DD.log |
One line per step of every run |
logs\history.tsv |
Timestamp, file, status, post ID, link, title |
logs\topics-used.txt |
Topic rotation state |
Troubleshooting
401 / "check -user and -password" — the password must be an application
password, and -user is the WordPress username, not the email address.
403 when publishing — the account can create drafts but not publish; use
an Editor account or -status draft.
404 / "REST API was not reachable" — confirm https://your-site/wp-json/
loads in a browser. If the site has no pretty permalinks, add -rest-route. A
security plugin may also be blocking the REST API for non-cookie auth.
Claude produced a question instead of an article — the reply is saved in
output\rejected\ and nothing is published. Usually the prompt left {{TOPIC}}
empty without telling Claude what to do in that case.
The scheduled task runs but nothing happens — check
logs\run-YYYY-MM-DD.log. If it is missing entirely, the task never started the
script: confirm the working directory is the repository root and that the task
is set to run only when you are logged on.
Layout
cmd/wpost/main.go CLI: flags, article -> API payload
internal/content/ front matter parsing, Markdown -> HTML
internal/wordpress/ REST client: posts, categories, tags
scripts/New-BlogPost.ps1 generate -> save -> publish
scripts/Register-BlogTask.ps1 scheduled task registration
prompts/post-prompt.txt the prompt you edit
Run the tests with go test ./....