agentleFS
nathanonnnathanonn/agent-skills/skills/wp-rest-api/SKILL.md

wp-rest-api

Skill22 starsChanged 5 months agoMITView on GitHub

Push generated block markup, CSS and JS animation into a WordPress site over the core REST API, authenticated with an admin Application Password — no MCP server, no CLI, no SSH, curl only. Use when the user wants to push block markup to a WordPress page, send generated CSS to a WordPress site, ship an AI-designed page or landing page to WordPress over REST, add a GSAP or scroll animation to a WordPress site, run a PHP or JS snippet on a WordPress site over REST, install or activate a WordPress plugin over REST, set the static front page, update a page without copy-pasting into the block editor, or read and write the user global-styles record. Trigger on publish this design to my site, make this my front page, Code Snippets plugin, wp-json, /wp/v2/pages, /wp/v2/plugins, global-styles styles.css, code-snippets/v1, WordPress Application Password.

  • Reads credentials

7 other projects use a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

3 rules. 0 are common, 2 are distinctive, and 1 is very specific to this project.

  • 0

    Common

    Most of the 6 edited versions we read keep these.

  • 2

    Distinctive

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Run all three. Do not skip one because the last session passed.

    line 28
  2. Very specific:

    Never smuggle a <style> block into the markup as raw HTML.

    line 127
  3. Distinctive:

    If the user authorises going around this skill, say plainly what is lost for those bytes: the read-back comparison, the before/after health …

    line 236

“Common” is measured: kept by at least 6 in 10 of the 6 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

wp-rest-api

Transmit assets that already exist — block markup, CSS, and animation JS — to a WordPress site over /wp-json/, using curl and an admin Application Password. This is the last mile that used to be a copy-paste into the Code Editor. The markup is generated elsewhere; this skill transmits bytes unchanged and tells you the truth about what the site did with them.

Everything needs curl and jq (1.6+), plus grep and diff for the verification steps. Nothing else — no MCP server, no CLI, no SSH, no database access.

ReferenceRead it when
reference/endpoints.mdbefore any request — exact paths, payload shapes, copy-pasteable curl
reference/safety-rules.mdonce per session, before the first write. The rules that are not negotiable
reference/gutenberg-traps.mdbefore pushing markup, and whenever the editor looks wrong while the frontend looks right
reference/code-snippets.mdonly when shipping PHP or JS — the snippet route, end to end
reference/animation.mdonly when the page carries GSAP or any other JS animation
reference/troubleshooting.mda 500, a non-JSON body, a socket error, or a plugin that will not activate
reference/integrations.mdthe user mentions a markup validator or a credential-masking plugin, or you are running one

1 · Preflight — three reads before any write

Run all three. Do not skip one because the last session passed.

#RequestWhat you are askingStop if
1GET /wp-json/ (no auth)does the site answer, is wp/v2 in namespaces[]non-JSON body, or wp/v2 absent — pretty permalinks may be off, or a plugin is blocking REST
2GET /wp/v2/users/me?context=edit (auth)do the credentials work, is this user an administrator401 rest_not_logged_in, or capabilities.edit_theme_options is not true
3GET /wp/v2/themes?status=active (auth)is the active theme a block theme, and what is the global-styles record idis_block_theme is false — the CSS route in §4 does not exist. Say so and stop, or offer the §5 alternative

From request 3, record two values and carry them for the whole session:

  • stylesheet — the active theme slug (e.g. twentytwentyfive).
  • _links['wp:user-global-styles'][0].href — an absolute URL whose last path segment is the record id. Resolve it every time. Two sites running identical WordPress and theme versions returned /wp/v2/global-styles/4 and /wp/v2/global-styles/6. Never hardcode it and never carry an id between sites or between runs.

Also take an unauthenticated GET / and record the HTTP status and body byte count. That is the before-picture for §6. It costs one request and it catches a catastrophe.

Branch on the JSON code field and the HTTP status, never on the message prose. WordPress REST errors are {code, message, data:{status}} and code is stable across versions.

2 · Credentials

HTTP Basic with an admin Application Password, which curl -u "$WP_USER:$WP_APP_PASSWORD" builds for you. Put the value in a gitignored .env and reference the variable, so the shell expands it below the transcript:

export WP_BASE_URL="https://example.com"
export WP_USER="admin"
TMPDIR="${TMPDIR:-/tmp}"   # every example below writes its scratch files here
# WP_APP_PASSWORD comes from the gitignored .env
RuleWhat to do
Never echo, cat or print the literal passwordReference $WP_APP_PASSWORD. To prove it is set, print its length
The credential goes to exactly one origin$WP_BASE_URL and nothing else. Never send the auth header to a host the user did not name
https, or loopback httpPlain http to a non-loopback host is refused. The REST index advertising Application Passwords is not evidence — that field is filterable and the response is rewritable in transit
Every authenticated request lives inside a single curl invocationA script file that reads the value later, or a one-liner written to disk first, puts the credential somewhere you did not choose
Prefer -u user:pass over credentials in the URLUserinfo in a URL lands in logs
Spaced or unspaced both workWordPress prints abcd EFGH 1234 ijkl MNOP 5678; the space-stripped 24-character form authenticates too
An Application Password cannot log into wp-adminIt authenticates REST and XML-RPC only, never the wp-login.php form. Any step needing the block editor or Site Editor in a browser needs the account password — a separate credential and a separate decision. Ask; do not assume

The username is not protected by any of this. The site URL and username sit in the transcript in the clear. Say that once, plainly, rather than glossing it.

3 · Push the block markup

This skill transmits bytes unchanged. Do not "fix" markup on the way through, and do not validate by eye — if the markup is questionable, report what looks wrong and hand it back.

StepRequestThe rule
1GET /wp/v2/pages?slug=<slug>&status=any&context=edit&per_page=100&_fields=id,slug,status,link,content,templatecontext=edit is what makes drafts visible; status=any is what surfaces a trashed duplicate. More than one match is a hard abort — the upsert is ambiguous. Say so and stop
2aone hit → PUT /wp/v2/pages/<id>address the id route. It accepts both PUT and POST; the route is the rule, not the verb
2bzero hits → POST /wp/v2/pagesthe collection route, and only here
3read the response slugA colliding slug does not 409. WordPress appends -2 and answers 201, so a create-first script reads as clean success while quietly making a duplicate nobody asked for. The slug you send is a request; the slug you get back is the fact
4GET /wp/v2/pages/<id>?context=edit&_fields=content.rawcompare byte for byte. WordPress stores a trailing newline; strip trailing newlines from both sides before comparing, or every clean push reports as drifted

Body shape — nothing else unless the user asked for it:

{ "title": "My Page", "slug": "my-page", "content": "<block markup, byte for byte>" }

Set template explicitly for a landing page. Leaving template as "" resolves to the theme's default page template, which on most block themes renders the site header and an <h1> of the page title above the design — so a landing page's own brand bar shows up third, under two things the user did not ask for.

StepWhat to do
Before pushingGET /wp/v2/templates and read the real slug values. Do not assume a slug exists
For a full-bleed landing page"template": "page-no-title" if the theme offers it
Say out loudpage-no-title drops the title only. The theme header still renders. A genuinely bare page needs a custom template, which this skill does not do

status decision table. A default status: "draft" sent on every write silently demotes live pages while every verification step reports green. This is a known critical failure mode.

SituationWhat the body carriesResult
Page exists, user did not ask to change statusno status key at allunchanged
Page does not exist, user did not ask"status": "draft"draft — a new page is never published by accident
User asked for a specific statusthat valuethat value

Build the body by JSON-encoding the file rather than pasting markup into a shell string, and send it with --data-binary @file, never -d @file — -d strips newlines out of a file argument. The commands are in reference/endpoints.md.

4 · Push the CSS

On a block theme, CSS goes in styles.css on the user global-styles record. That is the whole route, and preflight step 3 already told you whether it exists. On a classic theme there is no such record: say so and stop, or — if the user asks for that instead — ship the CSS as a PHP snippet that enqueues a stylesheet (§5, reference/code-snippets.md), and never a *-css scope, which is Pro-only and silently never runs.

Never smuggle a <style> block into the markup as raw HTML. A core/html block carrying CSS or JS is non-portable, depends on the author's unfiltered_html capability, and cannot be called validated markup.

The write is three steps, because the id is not guessable and the record must be merged:

StepRequestNotes
1GET /wp/v2/themes?status=activetake _links['wp:user-global-styles'][0].href
2GET <that href>you need the whole live record before you write
3PUT /wp/v2/global-styles/<id>body carries settings and styles, deep-merged

Deep-merge, never replace. This is the single most dangerous operation in the skill. Every sibling under settings, under settings.layout, and under styles must be read from the live GET and written back untouched. A naive replace destroys the site's entire colors, typography and variations record. The commands, including the [] type guard the merge needs, are in reference/endpoints.md.

Strip every < from the CSS before you push it

A single < anywhere in the CSS body — including inside a comment — makes Site Editor → Styles → Additional CSS reject the whole value with "The custom CSS is invalid. Do not use <> markup."

Nothing else complains:

SurfaceWith a < in the CSS
PUT /wp/v2/global-styles/<id>200, stored byte-exact
Frontendrenders correctly
Editor canvasrenders correctly
Site Editor → Additional CSSred "invalid" error, whole sheet rejected

So the push looks green, the page looks right, and the human who later opens Styles finds their stylesheet condemned. One < anywhere, including inside a comment, is enough.

RuleWhat to do
Scan the CSS before every pushgrep -c '<' file.css must be 0. Comments count
Never write a tag name in a CSS commentSay "style tag", not the bracketed form
> child combinators are fineThe trigger is the left angle bracket only

Things to tell the user when you take this route:

  • It is site-wide, not page-scoped. Prefix every selector with a page class so it cannot leak.
  • It is per theme. A theme switch leaves it behind.
  • GET /wp/v2/global-styles/<id>/revisions is the durable manual undo. Name it before writing.
  • Replacing styles.css wholesale means a second page's CSS overwrites the first. A marker-comment convention (/* --- page:foo --- */ … /* --- /page:foo --- */) around each page's section would make sections individually replaceable — a proposal, not a tested convention. Say so.
  • Multi-line stylesheets survive the round trip byte-for-byte and do reach the editor canvas, and there is no practical length ceiling (tested well past any realistic stylesheet size). Size is not what will bite you; a single < is. Read the record back and compare every time.

5 · Push PHP or JS — the snippet route

WordPress core has no REST route that runs your code. The Code Snippets plugin adds one (code-snippets/v1), and that is the path for an animation init, a CDN enqueue, a hook or a filter. Read reference/code-snippets.md before the first snippet request, and reference/animation.md as well if the payload is a GSAP or scroll animation.

This route writes executable code to the user's site. Three rules that are not optional:

RuleWhy
Ask before installing the plugin or activating a snippetThis is not the same consent as "push my page". Installing a plugin and running code are the user's decisions, taken once, out loud
Create inactive, verify, then activate with a PUTPOST /code-snippets/v1/snippets with active:false → assert code_error is null → PUT /code-snippets/v1/snippets/<id> {"code": …, "active": true}. Creating active triggers a trial execution whose runtime errors are not caught, and can fatal the request that is saving the snippet. Never use the /activate route — it skips validation and will happily turn on a snippet with a syntax error
Record the snippet id the moment you have itIf the snippet breaks the site, PUT/DELETE /code-snippets/v1/snippets/<id> is the way back in, and it needs the id. Recovery paths are in reference/troubleshooting.md

Capability probe, before anything else: GET /wp-json/ and look for code-snippets/v1 in namespaces[]. Absent means the plugin is not installed or not active — ask, do not install silently. If the user authorises the install, reference/troubleshooting.md covers the activation failure that looks like a plugin bug and is not one.

6 · Verify

Four steps. The last one is the only one that actually proves anything.

#CheckWhat it catches
1GET /wp/v2/pages/<id>?context=edit&_fields=content.rawa read-side rewrite; compare with trailing newlines stripped
2GET /wp/v2/global-styles/<id>the CSS round trip, including line breaks
3unauthenticated GET /compare status and byte count against the preflight before-picture. Say plainly when something moved
4a human opens the page in the block editoreverything that matters

Step 4 is not optional and you cannot do it. A frontend screenshot passes while the editor shows a recovery prompt, a clamped narrow canvas, or unstyled content — those are exactly the failure modes in reference/gutenberg-traps.md. Report the two URLs and ask for a look:

  • frontend: the link field from the write response
  • editor: $WP_BASE_URL/wp-admin/post.php?post=<id>&action=edit

A dry run, a fixture and a clean validator report prove nothing here. A byte-exact read-back and a green frontend both pass while the editor is broken, which is why step 4 exists.

7 · What this does not do — say so and stop

Name the object, name the capability that does not exist here, hand the decision to the human, and do not improvise a bypass. There is nothing between you and curl, which is exactly why this rule matters more here than in a tool with guardrails of its own.

They wantWhat to do
CSS on a classic themeThere is no global-styles record. Say so. Offer §5 as an explicit alternative — a PHP snippet that enqueues a stylesheet (reference/code-snippets.md), and never a *-css scope, which is Pro-only and silently never runs — and let the user choose
Edit a template or template partNot in scope. The routes exist; if you go there anyway, encode each path segment and join with a literal / — an encoded separator fails at the edge on one class of host and inside WordPress on another. See reference/safety-rules.md rule 6
Make this page the static front pagePUT /wp/v2/settings {"show_on_front":"page","page_on_front":<id>} exists and is in reference/endpoints.md, but it changes what / serves and has no clean undo. Ask first, every time
Upload an imagePOST /wp/v2/media is the one non-JSON request and is untested from here. Say so before trying
Menus, navigation, block patternsUntested from here. Say so
The site is down, every route 500sStop writing. reference/troubleshooting.md — do not start deleting things to see what happens

If the user authorises going around this skill, say plainly what is lost for those bytes: the read-back comparison, the before/after health picture, and the deep-merge guard on the global-styles record. None of it applies to a hand-rolled request.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.