generate and post wordpress posts using claude
  • Go 60.1%
  • PowerShell 39.9%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-05 01:36:04 +03:00
cmd/wpost initial commit 2026-09-05 01:36:04 +03:00
internal initial commit 2026-09-05 01:36:04 +03:00
prompts initial commit 2026-09-05 01:36:04 +03:00
scripts initial commit 2026-09-05 01:36:04 +03:00
.gitignore initial commit 2026-09-05 01:36:04 +03:00
go.mod initial commit 2026-09-05 01:36:04 +03:00
go.sum initial commit 2026-09-05 01:36:04 +03:00
README.md initial commit 2026-09-05 01:36:04 +03:00

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 ./....