# MiriamSchwab.me — Full Content > Source: https://miriamschwab.me/llms-full.txt > Generated: 2026-08-31 --- # WordPress Core AI in Practice: From the Abilities API to Claude as an Agent URL: https://miriamschwab.me/how-wordpress-ai-actually-works/ --- title: "WordPress Core AI in Practice: From the Abilities API to Claude as an Agent" date: 2026-08-30 modified: 2026-08-28 author: "Miriam Schwab" url: "https://miriamschwab.me/how-wordpress-ai-actually-works/" markdown_url: "https://miriamschwab.me/how-wordpress-ai-actually-works.md" type: post excerpt: "WordPress now has two AI systems that point in opposite directions: one where WordPress calls an AI, and one where an AI calls WordPress. Here's how each actually works as of WordPress 7.1, and why the Abilities API sitting underneath them is the part that matters." categories: - "Artificial Intelligence" - "WordPress" tags: - "abilities-api" - "agent-readiness" - "ai" - "mcp" - "php" - "wordpress" --- The [WordPress AI team](https://wordpress.org/news/2025/05/announcing-the-formation-of-the-wordpress-ai-team/) has been working on native integrations for AI, based on the “[building blocks](https://make.wordpress.org/ai/2025/07/17/ai-building-blocks/)” approach they laid out in July 2025. AI in WordPress has **two distinct layers**: 1. The first is what I’ll call the **“in-editor” AI**: features that appear as UI elements in the content management experience, like category suggestions and alt text generation, surfacing directly in the editor while you’re writing. 2. The second is the **agent channel**, which is a way for an external AI tool like Claude to connect to your site, discover its capabilities, and take actions on your behalf. ![Diagram of the two AI layers in WordPress. In-editor AI calls out to an external AI provider, while the agent channel receives calls in from an MCP client like Claude — opposite directions, both drawing on the same Abilities API in WordPress core.](https://miriamschwab.me/wp-content/uploads/2026/08/wp-ai-two-layers-1024x926.webp)The two layers run in opposite directions. Everything each one needs follows from who places the call.Here are the building blocks. All four now exist, though they’re at very different levels of maturity: [**PHP AI Client SDK**](https://make.wordpress.org/ai/2025/07/17/php-ai-api/) — a shared infrastructure Composer package that handles AI provider connections for any WordPress plugin. Plugin developers build against the PHP AI Client rather than rolling their own provider integrations; site owners configure providers once for their site rather than repeatedly per tool or plugin. [**Abilities API**](https://make.wordpress.org/ai/2025/07/17/abilities-api) — in some ways the star of the show. It’s what turns WordPress functionality into a discoverable, interactive system for AI agents, automation tools and more. It’s useful not just for LLMs, but for extending and working with WordPress overall. [**MCP Adapter**](https://make.wordpress.org/ai/2025/07/17/mcp-adapter) — this is specifically and only for the agent layer. It allows external agents to connect and interact with WordPress sites. The agent’s capabilities depend entirely on the abilities exposed via the Abilities API. [**AI plugin**](https://make.wordpress.org/ai/2025/07/17/ai-experiments-plugin/) (formerly the AI Experiments plugin) — the building blocks above are the “rails” of this initiative, and you can install and activate all of them without ever seeing an AI feature in your site. The AI plugin is what puts AI in front of users, adding its own settings screen and the UI elements in the content management experience that let people actually get AI assistance in their work. ## The foundation: WordPress’ Abilities API Everything starts with the [**Abilities API**](https://developer.wordpress.org/news/2025/11/introducing-the-wordpress-abilities-api/), which shipped in [WordPress 6.9](https://wordpress.org/download/releases/6-9/) and was expanded substantially in [7.1](https://make.wordpress.org/core/2026/07/31/abilities-api-improvements-in-wordpress-7-1/). It’s a standardized registry where plugins, themes, and WordPress core can declare what they’re capable of in a machine-readable format. Each ability has a name, a typed input and output schema, a permission callback, and an execute callback. Think of it as a formal, discoverable API layer sitting on top of everything WordPress can do. It’s the shared registry that both AI layers (in-editor and agent) draw from, though they use it in different ways. This direction [is the same open-extensibility move WordPress has always made](https://miriamschwab.me/why-mcps-are-the-hooks-of-the-ai-era/), just aimed at a new kind of consumer. Core ships three abilities: `core/get-site-info`, `core/get-user-info`, and `core/get-environment-info`. They live in two core categories, `site` and `user`. WordPress 7.1 gave all three consistent, translatable schemas and an optional `fields` parameter, so a caller can ask for just the fields it needs instead of the whole payload — a small change that makes an impact when the caller is paying for every token it reads. The registry also has its own REST API, independent of anything AI-related: `/wp-json/wp-abilities/v1/abilities` lists what’s registered, `/categories` lists the categories, and `/abilities/{name}/run` executes one. Whether a given ability shows up there is controlled per-ability by a `show_in_rest` meta flag. Registered is not the same as exposed — a distinction that comes back with MCP below. Three abilities is a deliberately thin starting point. The [WordPress AI plugin](https://wordpress.org/plugins/ai/) (more on that below) registers more to back its own features, and now ships an Abilities Explorer for browsing all Abilities registered on your site, plus support for registering custom abilities. But the real “magic” (that hasn’t yet been widely embraced by developers) [is when plugins ship abilities of their own](https://miriamschwab.me/wordpress-ready-for-agentic-web/). Worth noting: the Abilities API wasn’t designed exclusively for AI. It’s a general-purpose integration primitive — the stability of its schema makes it valuable for any kind of integration, whether that’s AI agents, CLI tools, REST endpoints, or automation pipelines. That said, AI is clearly what’s driving development, and in practice most of the abilities being built will be for AI use, so for most developers and site owners, AI-readiness is the story here. ## Layer one: in-editor AI Two separate pieces work together here: the [**Connectors API**](https://make.wordpress.org/core/2026/03/18/introducing-the-connectors-api-in-wordpress-7-0/), which landed in WordPress 7.0 core, and the **AI plugin**, which you install yourself. The Connectors API adds a Settings → Connectors screen to the WP Admin. It’s described as: “All of your API keys and credentials are stored here and shared across plugins. Configure once and use everywhere.” Anthropic, Google, and OpenAI are featured there and install in a click, but the provider implementations are separate connector plugins. Configure a key once and every feature on your site that wants a model can use it. One thing to know before you paste a key in: in the current release those keys are stored in the database unencrypted, though they’re masked in the UI. WordPress checks environment variables and PHP constants before it checks the database, so that’s the safer place to put them. The AI plugin has since added key encryption of its own. How it works technically: WordPress makes a server-side PHP call to the AI provider’s API. The intelligence lives in the cloud, but the interaction is triggered and mediated entirely by WordPress. **WordPress is calling the AI.** More connectors can be created, and have been. [The community has released connectors](https://make.wordpress.org/ai/2026/03/25/call-for-testing-community-ai-connector-plugins/) for [Grok](https://wordpress.org/plugins/aslams-ai-provider-for-grok/), [Mistral](https://wordpress.org/plugins/ai-provider-for-mistral/), [Ollama](https://wordpress.org/plugins/ai-provider-for-ollama/), and more. The AI plugin uses that configured provider to power contextual assistance while you write, and it has grown a lot: alt text generation, content classification, excerpt generation, title and slug generation, meta descriptions, summarization, translation, editorial notes, comment moderation, and image generation and editing, among others. Everything is opt-in through an experiment framework, so each feature has to be enabled explicitly. Once I turned things on, what showed up in the post editor was category and tag suggestions in the sidebar, excerpt generation, Generate Featured Image, Generate Meta Description, editorial notes, Generate Summary, and a title generation button that appears when you click into the title field. My experience with this: - Category and tag suggestions work and can be useful, but they’re not perfect. For example, it suggested the irrelevant “Static Site” category for a post about WordPress AI infrastructure. - Image generation requires a provider that supports it. At first I had only connected an Anthropic API key, but Claude is text-only. For AI-generated featured images you need Google or OpenAI, so I added an OpenAI key too and then image generation worked. - I asked it to create a featured image for a post, and it created something related, but terrible. I’m not sure what exactly the “prompt” is that gets passed from the post to the AI call, but it’s not effective. It would be better if you could see the prompt first and tweak it before submission, or adjust it after seeing a version of the image. - I tested image generation in the Media Library, and that gave better results, probably because I was able to enter the prompt manually. - There are some other useful features in the Media Library. You can click on an image and generate alt text right there, and when you save a generated image to the library it writes alt text for it in parallel. ### A thought about AI token usage Image generation is not cheap, and once I saw how resource-intensive a single request was, it made me wonder how we prevent LLMs from “running away” with opaque actions that rack up token costs. The plugin has since grown two features aimed at exactly that: AI request logging, so you can see what was actually called, and connector approvals, so a request has to be authorized before it goes out. ## Layer two: the agent channel The second layer runs in the opposite direction, and this is the one I actually use. Instead of WordPress calling an external AI, an external AI calls WordPress. You have a conversation, you direct Claude to do something, and Claude acts on your site through a standardized channel. **The AI is calling WordPress.** This is made possible by the [**MCP Adapter** plugin](https://github.com/wordpress/mcp-adapter), which takes anything registered in the Abilities API and exposes it via the Model Context Protocol. MCP is the open standard that lets AI tools like Claude discover and call external capabilities. With the MCP Adapter installed, your WordPress site becomes an MCP server. Note that the adapter is still GitHub-only — it’s not in the WordPress.org plugin directory, so you won’t get update notifications for it — and still pre-1.0, so it’s got a way to go. The server lives at `/wp-json/mcp/mcp-adapter-default-server`, and clients can point at it over HTTP directly. For Claude Desktop you can also go through a proxy package called `@automattic/mcp-wordpress-remote`: ``` "wordpress-yoursite": { "command": "npx", "args": ["-y", "@automattic/mcp-wordpress-remote@latest"], "env": { "WP_API_URL": "https://yoursite.com/wp-json/mcp/mcp-adapter-default-server", "WP_API_USERNAME": "your-wp-username", "WP_API_PASSWORD": "your-application-password" } } ``` A few things that aren’t obvious: **The adapter doesn’t expose one MCP tool per ability.** This surprised me. The default server exposes three meta-tools — `discover-abilities`, `get-ability-info`, and `execute-ability` — and the agent uses those to find and run whatever is registered. So the tool list stays the same size whether you have three abilities or fifty, and the agent looks up what it needs when it needs it. **Abilities aren’t exposed to MCP by default.** An ability needs a `public` flag in its meta (either `meta.public` or `meta.mcp.public`) before the adapter will surface it. When I first set this up, the three core abilities didn’t carry that flag and I needed a filter to add it. As of WordPress 7.1 core sets it itself, so that workaround is no longer necessary — but anything you register yourself still needs the flag set explicitly. **Strip the spaces from application passwords.** If you go the proxy route: when you generate an application password in WordPress (under Users), it’s displayed with spaces for readability, e.g. `xGnK JyJZ PPCa wnZ7`. Paste that as-is into `WP_API_PASSWORD` and the proxy won’t strip them for you — authentication just fails, silently. ## Building custom abilities Core’s three abilities, plus whatever the AI plugin registers, won’t get you far. For real content management work — listing posts by category, creating drafts, updating excerpts in bulk, managing tags — you need to register your own. Each ability’s `execute_callback` calls native WordPress PHP functions directly: `get_posts()`, `wp_insert_post()`, `wp_update_post()`, and so on. The Abilities API is not a wrapper around REST endpoints — the abilities themselves are just PHP. Core exposes the registry over REST, and the MCP Adapter exposes it over MCP, but those are two different views onto the same registry, not the registry itself. The pattern looks straightforward but has a few silent failure modes that cost time. **The hook sequence matters and is not documented clearly.** Register categories on `wp_abilities_api_categories_init` and abilities on `wp_abilities_api_init`. Using `init` at any priority returns null silently — the registry isn’t ready and there’s no error. **Your category has to exist first.** Passing a category that isn’t registered returns null, with no complaint. And there’s no `core` category to attach to even if you wanted one — core registers `site` and `user`, and that’s it. Register your own. **The category field is required.** Omitting it causes the same silent null. Here’s the structure that works: ``` add_action( 'wp_abilities_api_categories_init', function() { wp_register_ability_category( 'myplugin', [ 'label' => 'My Plugin', 'description' => 'Abilities for my plugin.', ]); }); add_action( 'wp_abilities_api_init', function() { wp_register_ability( 'myplugin/get-posts', [ 'label' => 'Get Posts', 'description' => 'Retrieve posts with optional filtering.', 'category' => 'myplugin', 'input_schema' => [ /* ... */ ], 'output_schema' => [ /* ... */ ], 'permission_callback' => fn() => current_user_can( 'edit_posts' ), 'execute_callback' => function( $input ) { /* ... */ }, 'meta' => [ 'public' => true, 'show_in_rest' => true, 'annotations' => [ 'readonly' => true, 'destructive' => false, 'idempotent' => true, ], ], ]); }); ``` Those `annotations` are worth filling in properly. They tell an agent whether an ability reads or writes, whether calling it twice is safe, and whether it can destroy something — which is exactly the information a client needs to decide what to do without asking you first. WordPress 7.1 also added [hooks around ability execution](https://make.wordpress.org/core/2026/07/29/new-execution-lifecycle-filters-for-the-abilities-api-in-wordpress-7-1/). `wp_ability_invoked` fires at the start of every call, before validation and permission checks, and `wp_ability_validate_input` and `wp_ability_validate_output` let you supplement schema validation with your own rules. If you want an audit trail of what an agent actually did on your site, that’s where it goes. One thing worth thinking about when designing abilities: not every technically accessible ability makes a good AI tool. The meta-tool pattern means the raw count matters less than it used to, since the agent discovers abilities on demand rather than loading all of them up front. But the shape still matters. Fewer, more robust abilities perform better than many small granular ones — better to give the agent a single ability that handles categories and tags in one step than three separate abilities it has to chain together. Self-documenting descriptions, clear error messages with next steps, and predictable output shapes all matter too. Turns out that [as usual, building things for humans](https://miriamschwab.me/wordpress-ready-for-agentic-web/) is also good for machines: the same things that make an API good for humans also make it good for AI. ## What the agent channel unlocks Claude and I built a custom plugin called [MS WordPress Abilities](https://miriamschwab.me/plugins/ms-wordpress-abilities/) that extends the default set — one of several things [Claude and I have built on this site together](https://miriamschwab.me/building-my-wordpress-site-with-claude-ai/). We started with the three core abilities and a filter to expose them to MCP, which core has since made unnecessary. Then we built `get-posts` (retrieve and filter posts by category, tag, status, and search), `create-post` (create drafts from conversation notes), and `get-categories` (list all categories with post counts, which turned out to be immediately useful for spotting a duplicate “Artificial Intelligence” category that the AI plugin had created alongside the existing “AI” category). Building these taught me most of what’s in the section above: the silent null failures, the category requirement, the hook sequence. None of it is documented clearly, and it took quite a bit of trial and error. After running on these abilities for a while, things were going well, but I wanted Claude to be able to do all the things, without limitation. So [I gave Claude WP REST API access](https://miriamschwab.me/claude-wp-rest-api-access-with-limitations/), with guardrails in place of course. Once the agent connection is working, and especially once you’ve gone beyond the core set, the experience is genuinely good. Claude can do a lot for me: updating SEO metadata per post, finding posts that are missing featured images, finding images with no alt text and writing it, internal linking analysis. I can ask it to list all posts in a specific category and it reads from the actual site. I can ask it to create a draft from notes I share in the conversation and the draft appears in wp-admin. I can say “find all posts missing excerpts and suggest replacements” and it reads the post content, generates suggestions, and makes the changes directly. It’s also how I run [the monthly routine that keeps this site healthy](https://miriamschwab.me/monthly-routine-that-improves-site-health/). The sweet spot is work that would otherwise mean clicking through a lot of wp-admin screens: bulk content improvements, cross-post analysis, anything where the tedium is the actual obstacle. The permission model is native WordPress. Every ability has a `permission_callback` that checks user capabilities. I created a dedicated WordPress user for Claude with Administrator access, but you could scope it to Editor or a custom role with only the specific capabilities the agent needs. ## Two layers, two directions To sum up: the in-editor AI is WordPress calling an external AI API *(I manage things within the WP Admin)*. The agent channel is an external AI calling WordPress *(I manage things from within Claude)*. They run in opposite directions, which is why their dependencies are almost entirely separate. For **in-editor AI** you need: the AI plugin, at least one connector plugin (Anthropic, Google, and OpenAI install in a click from the Connectors screen), and your API key saved there. You do not need the MCP Adapter. For the **agent channel** you need: the MCP Adapter, an MCP client like Claude, and at least one ability actually exposed. That last one isn’t optional: an MCP server with nothing public on it gives the agent nothing to do. You do not need the Connectors screen or an API key, since the intelligence lives in the client, not in WordPress. The Abilities API sits underneath both, and abilities registered by any plugin are available to both channels. ## The bigger picture What we see here leans heavily towards infrastructure thinking rather than feature thinking. The Abilities API, the MCP Adapter, the PHP AI Client, and the Connectors API are all pieces of the same architecture: establish a standard and let the ecosystem build on it, just as WordPress has always done. [Agent Skills](https://miriamschwab.me/wordpress-introduced-agent-skills-which-is-big-deal/) are part of the same pattern. The foundations are solid, and 7.1 went almost entirely into deepening them rather than adding user-facing features. The features built on top, at least in the AI plugin today, are still rough in places: rate limit errors, inconsistent suggestion quality, and a few settings that don’t obviously surface in the editor UI. The architecture is still much stronger than the features. But the architecture is the part that matters most right now. It’s what makes it possible for Elementor, Yoast, WooCommerce, and any other plugin to add AI capabilities that share credentials, share the standard, and work with the same agents. The compounding effect of an open ecosystem where every plugin can participate in the AI layer is what WordPress’s open extensibility has always been about. The catch is that the compounding only happens if plugins actually show up. Core has done its part; [the ecosystem largely hasn’t yet](https://miriamschwab.me/wordpress-ready-for-agentic-web/), which is a much harder problem than shipping an API. --- *The plugin I built for this is [MS WordPress Abilities](https://miriamschwab.me/plugins/ms-wordpress-abilities/). It handles the category registration and custom post management abilities, including a two-step preview/apply pattern for safe agent-driven content updates. If you’d rather make your site legible to agents that aren’t logged in at all, that’s a different job — see [Make My Site Agent-Ready](https://miriamschwab.me/plugins/make-my-site-agent-ready/).* --- # MiriamSchwab.me API URL: https://miriamschwab.me/api/ --- title: "MiriamSchwab.me API" date: 2026-08-28 modified: 2026-08-28 author: "Miriam Schwab" url: "https://miriamschwab.me/api/" markdown_url: "https://miriamschwab.me/api.md" type: page excerpt: "Everything on this site is readable by a machine without an API key, an account, or a scraper. This page is the map: what is published, where it lives, and..." --- Everything on this site is readable by a machine without an API key, an account, or a scraper. This page is the map: what is published, where it lives, and how to call the one endpoint that does something rather than returns something. The body of this page is rendered from the running site rather than stored as text, so this Markdown twin carries the introduction only. Nothing is hidden by that — everything the page shows already exists in a form built for you to parse, and these are better sources than a prose transcription would be: - [openapi.json](https://miriamschwab.me/openapi.json) — every endpoint, its parameters, its request body and its error shape. - [auth.md](https://miriamschwab.me/auth.md) — how to get access, and how to retry the one write endpoint safely. - [.well-known/mcp.json](https://miriamschwab.me/.well-known/mcp.json) — the MCP server and its tools. - [llms.txt](https://miriamschwab.me/llms.txt) — what content is on this site. - — GET it for the contact endpoint's own schema, limits and single-use token. --- # WordPress is ready for the agentic web, but our ecosystem isn’t URL: https://miriamschwab.me/wordpress-ready-for-agentic-web/ --- title: "WordPress is ready for the agentic web, but our ecosystem isn’t" date: 2026-08-27 modified: 2026-08-31 author: "Miriam Schwab" url: "https://miriamschwab.me/wordpress-ready-for-agentic-web/" markdown_url: "https://miriamschwab.me/wordpress-ready-for-agentic-web.md" type: post excerpt: "I gave a talk at WordCamp US about whether WordPress is ready for the agentic web. My answer: core has the plumbing, the plugin ecosystem hasn't shown up yet, and most of the agent-readiness checklist we're all chasing does nothing at all right now." categories: - "Artificial Intelligence" - "WordPress" tags: - "agent-readiness" - "agentic-web" - "ai" - "specification-website" - "wordcamp" - "wordpress" --- On August 19 I gave a talk at [WordCamp US](https://us.wordcamp.org/2026/) in Phoenix called [“The Agentic Web Is Coming – Is WordPress Ready?”](https://us.wordcamp.org/2026/session/the-agentic-web-is-coming-is-wordpress-ready/). The [video is on YouTube](https://www.youtube.com/watch?v=35gk5eWui9E) and the [slides are here](https://speakerdeck.com/miriamschwab/the-agentic-web-is-coming-is-wordpress-ready). In it I look at what it means for a site to be agent-ready, what the technical implementation involves, whether WordPress supports agentic activity, and what impact agent readiness actually has on a site’s performance in AI. https://www.youtube.com/watch?v=35gk5eWui9E Here’s my full talk## Most web traffic is bots now More than half of web traffic is now bots rather than people. Most of that is training, not agents interacting with anything, but regardless, the share keeps climbing. Around 68% of US Google searches end without a click, which we all feel on our sites, and Google AI Mode is somewhere around a billion users with AI query volume roughly doubling. ![Slide showing that 57% of web traffic is now bots, not humans, sourced to Cloudflare Radar in June 2026, next to a Cloudflare Radar chart of bot versus human traffic.](https://miriamschwab.me/wp-content/uploads/2026/08/The-Agentic-Web-Is-Coming-—-Is-WordPress-Ready_-Miriam-Schwab.webp)All of this matters for WordPress, and WordPress, with its market share of over 40% of the web, matters for everyone else. Whatever happens to the web happens to us, and at that size, what we do shapes the web back. ## Read, query, act Agent readiness breaks into three things a site needs to support. 1. **Reading** – an agent accessing your content at all. 2. **Querying** – pulling a specific answer out of that content rather than swallowing the whole page. 3. **Acting** – an agent doing something on your site instead of a person. E-commerce is the obvious case here: find me shoes, buy the shoes. ![Slide titled "What does agent-ready mean?" with three cards: Read, Query, and Act.](https://miriamschwab.me/wp-content/uploads/2026/08/The-Agentic-Web-Is-Coming-—-Is-WordPress-Ready_-Miriam-Schwab-1.webp)Before any of that, though, there’s a fair question: why help AI, if the agents are just going to take my work and answer people without sending them to me (as per the above-mentioned low click-through rate from Google)? It’s a real objection, and blocking agents is a legitimate choice. There are ways to do it, and Cloudflare and others are building models where agents pay for access. The way I see it, it’s a question of relevance. This is the web we have now, and increasingly people find things through AI. If my site isn’t in there, it isn’t anywhere. It also isn’t all or nothing: the specifications let you say what agents may do with your content, so you can allow search and retrieval while refusing training. Being agent-ready doesn’t mean open season: it means you set the terms instead of being invisible. I want the models reading my writing, so that when someone asks about the things I write about, my site is one of the sources. Here’s what that looks like in practice. The Content Signals proposal splits agent access into three separate permissions instead of one blanket yes or no, declared per bot in robots.txt. This is what mine says: ``` User-agent: ClaudeBot Allow: / Content-Signal: search=yes, ai-input=yes, ai-train=no ``` `search` covers appearing in a search index. `ai-input` covers an agent reading the page live to answer someone’s question. `ai-train` covers using the content to train a model. Mine is yes, yes, and no, and I have the same block for GPTBot, PerplexityBot, GoogleOther, and a few others. Almost no provider acts on this signal today, so for now it’s a stated preference rather than an enforced rule. I set it anyway, because opting in later costs nothing, while opting out after a model has already trained on your writing doesn’t undo it. ![Slide reading "Agent-ready doesn't mean open season. It means you're the one setting the terms, instead of being invisible."](https://miriamschwab.me/wp-content/uploads/2026/08/The-Agentic-Web-Is-Coming-—-Is-WordPress-Ready_-Miriam-Schwab-2.webp)## The AI optimization loop I run on my own site My favorite resource for optimizing my site, both for general best practices and for agent readiness, is [specification.website](https://specification.website), built by [Joost de Valk](https://joost.blog/), the founder of Yoast. It collects the current web specifications, tracks whether each item is required, recommended, optional, or something to avoid, and keeps itself updated as things change. The best part is that it’s also an MCP server, so you don’t have to sit and read it; you can send your agent to read it for you. ![The specification.website homepage, headlined "What a good website does," with a navigation menu that includes an Agent Readiness section.](https://miriamschwab.me/wp-content/uploads/2026/08/Monosnap-The-Website-Specification-2026-08-27-15-54-47-1024x519.webp)That turns site optimization into a loop instead of a checklist you work through by hand, which I wrote about in more detail in [the monthly routine I run on this site](https://miriamschwab.me/monthly-routine-that-improves-site-health/). In my case, Claude reads the specification, connects to my WordPress site through the [WordPress MCP Adapter plugin](https://github.com/WordPress/mcp-adapter), analyzes what’s actually there, finds the gaps, and reports back. I run it as a scheduled routine in Claude Code, and it ranks what it finds by what will help my site specifically, not just by whether the spec says required or recommended. Claude knows my site is a content site, so it doesn’t push me toward things that would cost me valuable time and bring little benefit. ![Slide showing agent readiness as a four-step MCP loop: read the standard, read my site, find the gap, fix it, with an arrow curving back to the start.](https://miriamschwab.me/wp-content/uploads/2026/08/The-Agentic-Web-Is-Coming-—-Is-WordPress-Ready_-Miriam-Schwab-3.webp)Then Claude can implement the fixes, because my site has the WordPress AI Abilities it needs, thanks to my [MS WordPress Abilities plugin](https://miriamschwab.me/plugins/ms-wordpress-abilities/). Note that running this kind of loop needs ongoing attention and tweaking. I’ve had to re-tune the routine several times when I noticed it doing something slightly off, and I read the reports before approving anything. ## WordPress AI plumbing Since WordPress 7, core has what is needed for this type of workflow: the [Abilities API](https://developer.wordpress.org/apis/abilities-api/), which defines what an LLM can do on a site, and the [MCP Adapter](https://github.com/WordPress/mcp-adapter), which connects that to an agent. (There’s also the [WP AI Client](https://make.wordpress.org/core/2026/03/24/introducing-the-ai-client-in-wordpress-7-0/), but it’s more limited for this kind of work, closer to click-here-get-an-alt-tag.) ![Slide titled "WP AI plumbing gives us the tools": the Abilities API registers what a site can do, the MCP Adapter exposes it to outside agents, and the WP AI Client, greyed out, lets WordPress consume AI itself.](https://miriamschwab.me/wp-content/uploads/2026/08/The-Agentic-Web-Is-Coming-—-Is-WordPress-Ready_-Miriam-Schwab-4.webp)The catch is that core ships only three abilities out of the box, and they’re basic. If you upgrade to 7.0, install the MCP Adapter, and expect magic, the experience can be very underwhelming. While the infrastructure is real, we need to supply the tools that work on top of it and actually make it useful. I built [a plugin for my own site that adds the abilities](https://miriamschwab.me/plugins/ms-wordpress-abilities/) I wanted, and I keep adding to it whenever I hit something I can’t do from Claude Code. At a certain point I [wrapped the WordPress REST API as an ability](https://miriamschwab.me/claude-wp-rest-api-access-with-limitations/), because the ability format on its own is constrained, and I didn’t want any limits on what I can do on my site with an AI agent. Feeling uncomfortable about letting an agent loose on your live site is very reasonable, and it’s a real trade-off. AI is non-deterministic, so the same instruction can produce different results, and even with a lot of guardrails, agents can make decisions you didn’t want. The Abilities API helps here, because part of an ability’s configuration is declaring whether it’s destructive, which tells the agent to be careful with it. In all the time I’ve been using Claude on my site, it has deleted something it shouldn’t have exactly once, and my backup saved the day. In general, it’s a good idea to back up, and test on staging or local first. ## I was excited about being agent-ready, and then I wasn’t I put in a lot of work to make my site agent-ready: markdown versions of every page, llms.txt, the machine-readable references in the head and in robots.txt, and [a contact form rebuilt as a proper API endpoint](https://miriamschwab.me/my-contact-form-is-now-an-agent-ready-api/) with [WebMCP](https://developer.chrome.com/docs/ai/webmcp) support. As a result, my score on Cloudflare’s [isitagentready.com](https://isitagentready.com/) [went from 51 to 86](https://miriamschwab.me/how-i-improved-by-my-sites-ai-readiness-score-from-51-to-86/). (The only reason it’s not higher is that I refused to change DNS records, which would bring no value to my site or its readers and is relatively invasive and risky to do.) Then I tested whether any of these shiny agent-ready features were being used by agents. They mostly weren’t! It turns out that the agents aren’t waiting for us. If they only read sites that had done all this agent-ready stuff, they’d be stuck reading a tiny portion of the web, so they figured out their own way in. When I sent Claude Code to access my site, I watched it announce it was going to use its [WebFetch tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-fetch-tool), and then it went straight for the HTML of my pages, completely ignoring the four places where I reference the existence of .md file versions. It took the HTML and converted it to markdown itself, in milliseconds, preferring that to my files, and then it shaped its version around the question it was trying to answer. When I asked whether it had noticed that my site offers markdown, it said no. I’d think it would prefer my markdowns because they can be around 15 times smaller than the HTML pages they came from, which is the kind of efficiency I assumed a model would want. But nope. My contact form went the same way. I told Claude to reach out to me about speaking opportunities. It found the form API only because it read my blog post about building the API (!), not through any of the signals I’d planted for it. When I pushed it to use the endpoint, it filled in the front-end form instead. It did identify itself as an agent in the submission, which was a nice touch, and it did that because the API asks agents to say so. It clearly didn’t need its own special endpoint or WebMCP implementation – it knows how to fill out forms like the rest of us. Because I had put in all this work to make my form agent-ready, I implemented a thoroughly inelegant workaround, in the hopes that it would lead agents to use the darn thing: I wrote a line on my contact page, in the visible content, practically yelling at the agents to tell them the form is agent-ready, so they should use it! ![Slide reading "Claude takes content very seriously," beside a screenshot of my contact page with a highlighted line telling agents the form is also available as an API.](https://miriamschwab.me/wp-content/uploads/2026/08/The-Agentic-Web-Is-Coming-—-Is-WordPress-Ready_-Miriam-Schwab-6.webp)My testing led me to realize that LLMs read, and really care about, the actual content on your site. That’s where they look, not llms.txt, not the alternate link headers, and definitely not the four places where I politely pointed them to the files. ## Agent readiness is human readiness In the end, there’s nothing new under the sun, and there’s nothing new under the WWW. It’s the same story it’s been for the last 20 years: build user-friendly, accessible sites and make sure they have stable URLs, clean semantic HTML with a sensible heading structure, structured data, and markup without junk in it. In fact, if you look at the agent readiness checklist on specification.website, you’ll see it has 21 items, and only one is required: stable URLs. Yes, you should have URLs that lead somewhere, just like you always should have since the dawn of the web. That’s it. ![Slide titled "It makes your site better overall" with four cards: stable URLs, clean semantics, structured data, and clean markup, above the line "Bots want what humans want."](https://miriamschwab.me/wp-content/uploads/2026/08/The-Agentic-Web-Is-Coming-—-Is-WordPress-Ready_-Miriam-Schwab-7.webp)I had a bit of a crisis when I realized this, so I went to discuss it with some of the sharpest professionals in the SEO/GEO space. They said the same thing: the pages that win in agent answers are the ones that are easy to fetch, easy to parse, and easy to trust. Same old, same old. ![Slide titled "Agent readiness is human readiness," quoting that the pages winning in agent answers are easy to fetch, parse, and trust, and naming two common mistakes: treating agent readiness as separate from SEO and accessibility, and writing for the model rather than the reader.](https://miriamschwab.me/wp-content/uploads/2026/08/The-Agentic-Web-Is-Coming-—-Is-WordPress-Ready_-Miriam-Schwab-8.webp)Turns out there are some advantages to making your site agent-ready, not because it helps agents but rather because it helps humans. For example, rebuilding my contact form for agents made it more accessible to people too, and removed its JavaScript dependency along the way. The mistake I was making is the same one people made with SEO: writing for the machine instead of the reader. Make it good for humans and the agents do just fine. The more human-accessible a thing is, the more machine-readable it turns out to be too. This doesn’t mean we should ignore the agent-ready specifications; they’re in progress, and there’s a real chance the big models and browsers will converge on some of them. Several are cheap to implement and harmless to have. If someone flips a switch one day, sites that already did the work will be ready. ## Great questions during the Q&A I always love a good Q&A after I give a talk, and this time the audience really delivered. Someone asked whether we’ll end up writing hidden text for agents, the way screen reader text lives in a span. Probably not hidden, is my guess, because agents strip out everything around the main content when they convert a page. If we do this, it will have to live where humans can see it. On llms.txt, I told the person who asked what I had found: I could not get Claude to see mine. I put it everywhere it’s supposed to go, then put it in the footer twice out of sheer stubbornness, and it still didn’t care. Build it if you like, but it’s more cosmetic than anything else at this point. I liked the comparison someone in the audience made: this is [Google AMP](https://developers.google.com/amp) all over again. Side files drift out of sync with the original pages they represent, so the HTML becomes the source of truth. Same here with markdown files – you have to make sure they accurately reflect the original files. Or we’ll just end up where we did with AMP, going full circle back to good old HTML that works just fine for everyone, whether they’re humans or bots. ## Where the WordPress ecosystem is Our ecosystem, though, is lagging behind. My site has 67 abilities on it. Three come from core. I wrote 41 of them, spread across five plugins of my own. Another 21 come from [Angie, Elementor’s agentic AI plugin](https://elementor.com/ai-page-builder/), which added a large set of abilities in a recent release. From every other plugin on my site, including widely used ones, there are two, both from Yoast. Turn on the Abilities Explorer in your own WP Admin and you’ll probably see a much shorter list. Our ecosystem hasn’t picked this up yet, and adding abilities to a plugin isn’t hard. Doing it means people can manage your plugin through an agent, which is how I now manage most of mine. That gap is also the opportunity. WordPress is 23 years old, which is ancient for software, and this is one of the few moments where the ground is open again. The models know WordPress unusually well because it’s open source and they’ve been trained on all of our code and all of our writing about it. The CMS structure works well. The plugin architecture is a good fit for tools an agent can call. Building AI capability into an existing product, or building something new for the ecosystem, is available to anyone who wants to get ahead now. I built a handful of plugins for myself doing exactly this, and they’re listed at [miriamschwab.me/plugins](https://miriamschwab.me/plugins/) if you want to look. Fair warning: I’m the only person using them, so try them on a local or staging site first, and tell me what breaks. --- # Privacy URL: https://miriamschwab.me/privacy/ --- title: "Privacy" date: 2026-08-27 modified: 2026-08-27 author: "cloudagent" url: "https://miriamschwab.me/privacy/" markdown_url: "https://miriamschwab.me/privacy.md" type: page excerpt: "What this site collects, why, and what I do with it — including the agent request log, which most sites don't have." --- Short version: this is a personal site. I don’t sell your data, I don’t run ad networks on it, and most of what gets collected is the ordinary exhaust of running a website. Here’s the specific list. ## Analytics I use Google Analytics, loaded through Google Tag Manager, to see which pages people actually read. It sets cookies and records things like the pages you visit, roughly where in the world you are, and what browser you’re on. I look at it in aggregate — I’m not interested in identifying individual readers, and I don’t try to. ## Newsletter The signup form goes to Kit (formerly ConvertKit), which stores your email address and whether you opened anything. I use it to send The Diff and nothing else. Every email has an unsubscribe link, and unsubscribing removes you from the list — you don’t need to ask me. ## Contact form If you send me a message, it reaches my inbox and stays there like any other email. Name, email address, and whatever you wrote. I don’t add contact-form submissions to the newsletter list. ## The agent request log This one is unusual enough to spell out, because most sites don’t have it. This site publishes machine-readable versions of its content for AI agents and crawlers — `llms.txt`, Markdown copies of every page, an MCP server, and so on. When something fetches one of those, the site records what was fetched, when, the User-Agent string, and the IP address. That’s how I know which AI crawlers actually read this site, which is a thing I write about. It’s a log of automated traffic, not of readers browsing normally, though a request from your browser to one of those files would be recorded too. IP addresses count as personal data under GDPR, so: the log is stored on this site’s own database, it isn’t shared with anyone, and it isn’t used to build a profile of anybody. If you want an entry removed, email me and I’ll remove it. ## Hosting, CDN, and server logs The site runs on Elementor Cloud and sits behind Cloudflare. Both keep their own server logs, including IP addresses, for security and performance reasons — standard for any hosted site. Cloudflare also sets a `__cf_bm` cookie to tell bots from humans. ## Cookies The ones this site actually sets: Google Analytics cookies (see above), Cloudflare’s bot-management cookie, and a small preference cookie remembering whether you chose light or dark mode. If you log in to WordPress here — which is me — WordPress sets session cookies. There’s no advertising or cross-site tracking. ## What I don’t do No ad networks. No data brokers. No selling or renting anything to anyone. No profile-building. No dark patterns to get your email. ## Your rights If you’re in the EU, UK, or somewhere with similar law, you can ask me what I hold about you, ask for it to be corrected, or ask for it to be deleted. Email me and I’ll do it — there’s no form to fill in and no waiting period beyond however long it takes me to read my email. ## Changes If this changes materially I’ll update this page and note it in the [changelog](/changelog/). ## Contact Questions about any of this: use the [contact form](/contact/) or email me directly. --- # The Agentic Web Is Coming – Is WordPress Ready? URL: https://miriamschwab.me/media/the-agentic-web-is-coming-is-wordpress-ready/ --- title: "The Agentic Web Is Coming – Is WordPress Ready?" date: 2026-08-21 modified: 2026-08-29 author: "Miriam Schwab" url: "https://miriamschwab.me/media/the-agentic-web-is-coming-is-wordpress-ready/" markdown_url: "https://miriamschwab.me/media/the-agentic-web-is-coming-is-wordpress-ready.md" type: ms_media excerpt: "The agentic web is being built right now, and AI agents don’t browse it the way humans do. They need to read it, query it, take actions on it. It’s..." --- The agentic web is being built right now, and AI agents don’t browse it the way humans do. They need to read it, query it, take actions on it. It’s a new layer of the web – one where sites aren’t just destinations for human visitors but interfaces that AI systems can interact with directly. WordPress, which already powers 43% of the web, has a real structural role to play here. But it requires the ecosystem to think differently about what a WordPress site actually is. In this talk I dig into what the agentic web actually means in practice – what agents need from a site, how MCP and the WordPress AI infrastructure being built in core (WP AI Client, MCP Adapter, Abilities API) fit in, and what developers, agencies, and site owners should be thinking about now before this shift accelerates. [Full writeup of the talk here.](https://miriamschwab.me/wordpress-ready-for-agentic-web/) --- # My contact form is now an agent-ready API URL: https://miriamschwab.me/my-contact-form-is-now-an-agent-ready-api/ --- title: "My contact form is now an agent-ready API" date: 2026-08-13 modified: 2026-08-19 author: "Miriam Schwab" url: "https://miriamschwab.me/my-contact-form-is-now-an-agent-ready-api/" markdown_url: "https://miriamschwab.me/my-contact-form-is-now-an-agent-ready-api.md" type: post excerpt: "Most agent-readiness advice is about helping agents read your site. Here is what it took to let one actually do something: send me a message through my contact form, using the same endpoint the form itself uses." categories: - "Artificial Intelligence" - "WordPress" tags: - "accessibility" - "agent-readiness" - "contact-form" - "rest-api" - "wordpress" --- Most advice about making a site agent-ready is about how it will be read: publish clean markup, add structured data, serve your pages as markdown, write an `llms.txt`. But very little of it is about helping the agent **do** things. I wanted to see what an agent-ready contact form looks like in practice, so I picked the smallest useful thing an agent might want to do on someone’s behalf on my site: send me a message. Claude and I turned my [contact form](https://miriamschwab.me/contact/) ([built with native WordPress functions](https://miriamschwab.me/wordpress-forms-without-plugin/)) into a documented API, and now an agent can theoretically fetch its schema, ask for a token, and send me a message. Note how I said “theoretically”. I’ll get to that later. But first, here’s how we made the form agent-ready. ## One endpoint to rule them all At first I thought we should leave the existing form alone and bolt a separate API onto the side for agents. But I’m not a fan of duplicate functions (or duplicate anything), and in this case two paths would have meant two sets of validation, two sets of limits, and an agent path that could get stale and outdated because it’s not regularly used. With one path serving both agents and humans, the humans who submit my form are also acting as testers, which means every message a real person sends me is also a live test of the agent path. If the API breaks, the actual form breaks too, and I would be able to find out immediately. ## Discovery is the handshake If an agent wants to send a message via my form, it needs to complete two steps: GET the endpoint, which returns the input schema plus a single-use token that is good for fifteen minutes; then POST the message back with that token attached. Why two steps? An agent’s natural first move is to read the schema, because it needs to know the field names. Attaching the credential to that response means a well-behaved agent gets it for free, as part of a request it was going to make anyway. A script that blindly POSTs at the documented URL without ever reading the schema gets rejected. This serves as a small anti-spam measure. Here is what that GET returns, trimmed a little. This is the form describing itself, with no page and no HTML involved: ``` { "name": "contact_miriam_schwab", "title": "Send Miriam Schwab a message", "endpoint": "https://miriamschwab.me/wp-json/miriamschwab/v1/contact", "auth": "none", "human_confirmation_required": true, "rate_limit": { "per_ip_per_hour": 10, "site_wide_per_day": 25, "on_exceeded": "HTTP 429 with a retry_after hint." }, "submit_token": "", "input_schema": { "required": ["name", "email", "message", "submit_token"] }, "guidance": [ "Do a GET on this URL first. The response carries the submit_token you need.", "Confirm the exact wording with your user before sending. This reaches a human inbox and cannot be unsent.", "Do not send marketing, bulk, or automated outreach through this endpoint.", "Validation failures come back as HTTP 422 with a "fields" object naming each bad field." ] } ``` And here is what you get if you skip that step and POST straight at the URL: ``` { "code": "ms_contact_bad_token", "message": "Missing, expired, or already-used submit_token. Do a GET on this URL to get a fresh one, then retry.", "data": { "status": 401 } } ``` No skipping the line bro! ## This approach has accessibility benefits too Yet again, it turns out that best practices for building websites in general have a strong overlap with better accessibility, which is fantastic. When you make a form more readable for machines, it also makes it more accessible for screen readers. Agents operate a page from the same accessibility tree assistive technology uses so when one improves, the other does as well. Here are the accessibility improvements this update made to my form: - Every input now carries an `autocomplete` token. That is the single strongest signal for anything trying to figure out which field is the email box, and my form did not have it before this update. - Errors are attached to the field that caused them rather than dumped in a generic message at the bottom, so an agent can fix it instead of guessing. - Instead of the error only appearing visually, now a screen reader can actually **hear** which field is wrong. The form also works with JavaScript turned off now, which wasn’t the case before. This was implemented for the sake of agents, but in general also makes this a better built form overall. ## Agents in the browser should draft, not send There are two kinds of agents that can reach my form: one runs inside a browser with my HTML page loaded, like a sidebar assistant or an extension. The other calls over HTTP from somewhere else and never loads the page at all. Both can use the API, but only the first can see tools the page itself registers. That is what WebMCP is for: it lets a page hand an in-browser agent a set of callable tools through `navigator.modelContext`, the same way the API hands an outside agent a schema. For agents running inside the browser, my site registers two WebMCP tools. `contact_miriam_schwab` sends a message. `prepare_contact_message` fills in the form on the page and stops, leaving the send button to the person theoretically sitting there guiding the agent. The tool descriptions tell agents to prefer the second one whenever the form is on screen, and my endpoint declares in its own schema that the action needs human confirmation because a message to a real person cannot be unsent. WebMCP is still early, and no browser ships it unflagged yet. The script that registers these tools feature-detects before it loads, so browsers that cannot use it never download it at all. ## How to handle agent spam if it happens Publishing an API for my form didn’t create a new spam risk, but it did make it more findable. So I implemented a way to easily take the pointers down that direct agents to my form. The form endpoint is listed in `llms.txt`, in an `api-catalog` document, and in an Agent Skills index. I can remove it from any one of those, or all three, on a settings screen in my [Make My Site Agent-Ready](https://miriamschwab.me/plugins/make-my-site-agent-ready/) plugin that generates those documents. If it ever gets out of hand, the endpoint stops being advertised while people can carry on using the form. There are also rate limits, which limit how much damage a bot can do. ![The Make My Site Agent-Ready settings screen, showing the contact endpoint with its name, URL, description, and checkboxes for listing it in api-catalog, llms.txt, and Agent Skills.](https://miriamschwab.me/wp-content/uploads/2026/08/image-1024x725.png)The endpoint settings screen in the Make My Site Agent Ready plugin## What an agent can find If an agent lands on any page of my site, the response headers point at a description of the contact service. If it never loads a page at all and just surveys the site, it finds the endpoint in the three documents listed above. All of this is managed in one unified place. This matters because it prevents the discovery docs from drifting and disagreeing with each other. The header goes out on every page, so an agent that fetches anything at all can find the endpoint without parsing a line of HTML: ``` Link: ; rel="service-desc"; type="application/json"; title="Send Miriam Schwab a message" ``` In `llms.txt` it becomes a section of its own: ``` ## Agent Endpoints - [Send Miriam Schwab a message](https://miriamschwab.me/wp-json/miriamschwab/v1/contact): GET this URL first for the input schema and a single-use submit_token (valid 15 min), then POST it back with name, email and message. A POST without the token is rejected. Reaches a real inbox and cannot be unsent, so confirm the wording with your user first. (GET/POST · application/json · auth: none; POST needs a submit_token from GET) ``` And in the Agent Skills file it reads more like documentation: ``` ## Actions Beyond reading content, this site exposes endpoints you can call directly: - **Send Miriam Schwab a message** — `GET/POST https://miriamschwab.me/wp-json/miriamschwab/v1/contact` (application/json). GET this URL first for the input schema and a single-use submit_token (valid 15 min), then POST it back with name, email and message. A POST without the token is rejected. Reaches a real inbox and cannot be unsent, so confirm the wording with your user first. Auth: none; POST needs a submit_token from GET. ``` Three documents, one entry on that settings screen. ## Next steps for making your form agent-ready If you want to try this on your own site, do the boring stuff first: - Add `autocomplete` attributes. - Attach your error messages to the fields that caused them. - Put your success message somewhere it gets announced. - Make the thing work without JavaScript. None of that requires an API, an agent, or a new standard, and it makes your form better for people in general. Then, if you want agents to be able to act, give them a documented way in. I’m sure there are plugins and tools out there to help you, but you can also feel free to use my [Make My Site Agent-Ready](https://miriamschwab.me/plugins/make-my-site-agent-ready/) plugin to manage this. ## Important caveat: all of this is theoretical After implementing this new forward-thinking form, I ran thorough tests to see how agents would engage the form and use it. Turns out, they won’t use it and totally ignore the API I created for them. At one point I had published this very post, and once it was live the agents used it as a signal that the site has an agent-ready form. The agents never took a look at the headers, the llms.txt or any other signals I left for them. They ignore all of that. So at this point creating these types of agent-ready functionalities and interfaces on your site is just a practice in theory. It’s probably worth familiarizing yourself with this whole world of agentic web browsing in case one day the agents do decide to take a look at all the breadcrumbs we left, them, but in the meantime it’s just cosmetic. I’ll share more about that in a later post. --- # TIL: you can build a native WordPress contact form without a plugin URL: https://miriamschwab.me/wordpress-forms-without-plugin/ --- title: "TIL: you can build a native WordPress contact form without a plugin" date: 2026-08-05 modified: 2026-08-13 author: "Miriam Schwab" url: "https://miriamschwab.me/wordpress-forms-without-plugin/" markdown_url: "https://miriamschwab.me/wordpress-forms-without-plugin.md" type: post excerpt: "When Claude and I were working on building this WordPress site, I was really pleasantly surprised when it made the decision to build the content form using native WordPress functionality..." categories: - "WordPress" tags: - "ajax" - "contact-form" - "php" - "rest-api" - "wordpress" --- When Claude and I were working on building this WordPress site, I was really pleasantly surprised when it made the decision to build the content form using native WordPress functionality rather than using a plugin. I didn’t know that was possible! I always love when there’s an option to choose built-in features in WordPress, so this made me (nerdily) happy. I wrote about it back in my post on [building my site with Claude](https://miriamschwab.me/building-my-wordpress-site-with-claude-ai/#contact-form). Recently I learned that there is more than one way to build a form in WordPress when I came across a post by Chandra Patel on [handling forms with admin-post.php](https://chandra.dev/wordpress-admin-post-php-form-handling/), where he too shared that he was surprised by this capability. The approach he described works differently from my `admin-ajax.php` form, so I did some Claude-assisted research to learn more about this, and the pros and cons of each approach. During my research I learned that there’s even a third REST API option too! So here’s the breakdown of what each approach offers: ## How my contact form works My form uses WordPress’ [AJAX system](https://developer.wordpress.org/plugins/javascript/ajax/). When you fill it in and hit send, a bit of JavaScript catches the submission before the page can reload. It quietly bundles up your name, email, and message and sends them to a WordPress file called `admin-ajax.php`, along with a security token called a [nonce](https://developer.wordpress.org/apis/security/nonces/) that proves the request came from my actual form and not a bot hitting the URL directly. On the server, WordPress runs a small function I have in my theme. It checks the nonce, cleans up the submitted data, makes sure the email address is real, and then sends the message to me using WordPress’s built-in `wp_mail()` function. It sends back a simple yes or no. If it’s a yes, the page shows “message sent” right there, without reloading which gives a smooth user experience which I like. ## The other way: admin-post.php The approach in Chandra’s post skips the JavaScript. The form posts the old-fashioned way, straight to a WordPress file called `admin-post.php`. WordPress looks at a hidden field in the form to figure out which handler function should run (through a hook named [`admin_post_{$action}`](https://developer.wordpress.org/reference/hooks/admin_post_action/)), checks the nonce, sends the email, and then redirects you to a thank-you page or back to the form with a success message. The main difference here is the page reload. My version updates in place. This version does a full round trip: submit, process, redirect, new page. It’s the classic way web forms have worked, and has the advantage of not needing Javascript. ## Where each one wins The AJAX approach that Claude built me offers a nicer, smoother experience: nothing flashes or reloads, and the reply appears instantly. That’s why it’s so common on modern sites. The trade-off is that it leans on JavaScript. If someone has JavaScript turned off, or a script fails to load, my form quietly does nothing, which isn’t great, but for my tiny personal site, it’s not the end of the world. Because the admin-post.php approach doesn’t need Javascript, it’s more sturdy. It also handles the “did you want to resubmit this form?” browser warning cleanly, because it redirects you to a fresh page after sending. The cost is the reload, and showing a success message is a bit more fiddly, since you have to pass that message along through the redirect instead of just handing it back on the spot. I’ll just elaborate a bit on what the resubmission warning is about: with a plain form post, the page you land on is the direct response to your submission. The browser remembers that the page came from a POST. So if you refresh it, or hit the back button to return to it, the browser has to send your form data all over again to rebuild the page. It can’t tell whether doing that is safe, so it stops and asks you first. For example, in Chrome the dialog says something like “Confirm form resubmission.” On a contact form, clicking through it could fire off your message a second time. On a checkout page it could mean getting charged twice, which is why browsers are so careful about it. The redirect sidesteps this: after admin-post.php processes the form, it doesn’t leave you sitting on the POST response. It sends you on to a normal page loaded with a regular GET request, and that’s the page you actually see. Refresh it and the browser just re-runs a harmless GET. Hit back and you land on a GET page too. The resubmission dialog never comes up. This is an old, well-worn pattern with a name: Post, Redirect, Get, usually shortened to PRG. Note that this only applies to plain form posts, the kind that reload the page. A form that submits in the background with JavaScript never loads a new page from the POST, so there’s no POST response sitting in your history to resend, and the warning has nothing to trigger it. If you build a form the classic way without the redirect, though, the resubmission prompt is the thing you’ll run into, and PRG is the standard fix. Neither approach is more “correct.” They’re two built-in tools for slightly different jobs. If you want a slick, no-reload experience and you’re comfortable relying on JavaScript, AJAX fits. If you want a form that keeps working with JavaScript switched off, or you’re building something more elaborate like a settings page, admin-post.php fits. ## Restricting who can submit There’s a flip side to the logged-in question. Both admin-ajax.php and admin-post.php give you two hooks: one that fires for logged-in users and one for logged-out visitors. My contact form registers both, because I want anyone to be able to reach me. If you leave off the logged-out hook, though, only logged-in users can submit. That’s handy for a membership site, where a form should only work for people who are signed in. One thing to know if you build it that way: being logged in isn’t the same as being allowed. The hooks only check that someone is signed in, not what they’re permitted to do, so by default any logged-in user, right down to a basic subscriber, could submit. If you want to limit a form to certain roles, say editors and above, you add a `current_user_can()` check inside the handler. The REST API has its own version of this in the `permission_callback`, which is where you decide who gets through. One practical wrinkle with admin-post.php on a public page: some security plugins are wary of it. The file lives inside `/wp-admin/`, the admin area, and many security tools treat any request to wp-admin from a logged-out visitor as suspicious, because that’s what a lot of attacks look like. They might rate-limit it, show a challenge, or block it outright. `admin-ajax.php` sits in the same folder, but it’s been the standard front-end AJAX endpoint for so long that security plugins almost always leave it alone. Front-end use of admin-post.php is less common, so it’s more likely to trip a rule. It’s not a reason to avoid it, just something to test if you pair a public admin-post.php form with a security plugin. ## Turns out there’s a third way you can do this too! Once I started digging, I found a newer option which WordPress core has been pointing people toward for years: a custom [REST API](https://developer.wordpress.org/rest-api/) endpoint. With the REST API approach, instead of sending the form data to the shared `admin-ajax.php` file, you give your form its own dedicated address, something like `/wp-json/miriam/v1/contact`. You set that address up in your theme or plugin with a function called [`register_rest_route`](https://developer.wordpress.org/rest-api/extending-the-rest-api/adding-custom-endpoints/), and you tell WordPress which function should run when someone posts to it. The JavaScript on the page then sends the message there instead. From the visitor’s side it feels the same as my current form: fill it in, hit send, get a reply in place with no reload. The difference is under the hood. A REST endpoint is purpose-built for exactly this kind of request, so it doesn’t load the whole admin system on every submission the way `admin-ajax.php` does. It also comes with tidy, built-in spots to clean up the incoming data, check the nonce, and decide who’s allowed to submit, instead of writing all of that by hand. If I ever wanted something other than my contact page to talk to the same endpoint, say a mobile app or another site, REST would already be set up for it. It takes a little more setup to register the route in the first place (but is that really a factor when you’re building things with AI?), and it still relies on JavaScript for the no-reload experience. Having said that, if I were building the form today I’d definitely build it this way since it’s a cleaner foundation, and it’s likely I’ll switch over to this at some point down the line. ## The three contact form options, side by side in code Here’s the structure of what runs on my site now, trimmed down so it’s easy to follow. ``` // Register the handler for the "ms_contact" action. add_action( 'wp_ajax_ms_contact', 'ms_handle_contact' ); // logged-in users add_action( 'wp_ajax_nopriv_ms_contact', 'ms_handle_contact' ); // visitors function ms_handle_contact() { // 1. Verify the nonce (the security token sent with the form). if ( ! wp_verify_nonce( $_POST['nonce'] ?? '', 'ms_contact_nonce' ) ) { wp_send_json_error( 'Security check failed.' ); } // 2. Clean up the incoming data. $name = sanitize_text_field( $_POST['ms_name'] ?? '' ); $email = sanitize_email( $_POST['ms_email'] ?? '' ); $message = sanitize_textarea_field( $_POST['ms_message'] ?? '' ); // 3. Make sure the required fields are there and the email is real. if ( empty( $name ) || ! is_email( $email ) || empty( $message ) ) { wp_send_json_error( 'Please fill in every field with a valid email.' ); } // 4. Send it, and report back yes or no. $sent = wp_mail( get_option( 'admin_email' ), "Contact from {$name}", $message ); $sent ? wp_send_json_success() : wp_send_json_error( 'Could not send.' ); } ``` Chandra’s admin-post.php version handles the same four steps, but it hooks onto different actions and, instead of returning JSON, it redirects when it’s done. That redirect is the Post, Redirect, Get pattern from earlier, which is what keeps the resubmission warning from ever showing up. ``` // Register the handler for the "ms_contact" action on admin-post.php. add_action( 'admin_post_ms_contact', 'ms_handle_contact_post' ); // logged-in users add_action( 'admin_post_nopriv_ms_contact', 'ms_handle_contact_post' ); // visitors function ms_handle_contact_post() { // 1. Verify the nonce (submitted as a hidden field in the form). if ( ! wp_verify_nonce( $_POST['ms_contact_nonce'] ?? '', 'ms_contact_action' ) ) { wp_die( 'Security check failed.' ); } // 2. Clean up the incoming data. $name = sanitize_text_field( $_POST['ms_name'] ?? '' ); $email = sanitize_email( $_POST['ms_email'] ?? '' ); $message = sanitize_textarea_field( $_POST['ms_message'] ?? '' ); // 3. Make sure the required fields are there and the email is real. if ( empty( $name ) || ! is_email( $email ) || empty( $message ) ) { wp_safe_redirect( home_url( '/contact/?sent=invalid' ) ); exit; } // 4. Send it, then redirect to a fresh page so a refresh can't resend it. $sent = wp_mail( get_option( 'admin_email' ), "Contact from {$name}", $message ); wp_safe_redirect( home_url( $sent ? '/contact/?sent=1' : '/contact/?sent=error' ) ); exit; } ``` The steps in the middle are the same as before. What’s different is the ends: it hooks onto `admin_post_*` instead of `wp_ajax_*`, and it finishes with `wp_safe_redirect()` and `exit` rather than sending back JSON. Whatever page you redirect to then reads the `?sent=` value in the URL and shows the right message. And here’s the same idea as a REST endpoint. The middle three steps are almost identical. What changes is the setup around them: you register a dedicated route, and you hand back proper responses instead of the AJAX-only helpers. ``` // Register a dedicated endpoint: /wp-json/ms/v1/contact add_action( 'rest_api_init', function () { register_rest_route( 'ms/v1', '/contact', [ 'methods' => 'POST', 'callback' => 'ms_handle_contact_rest', 'permission_callback' => '__return_true', // public form: anyone may submit ] ); } ); function ms_handle_contact_rest( WP_REST_Request $request ) { // 1. Verify the nonce (sent as an X-WP-Nonce header from the page). if ( ! wp_verify_nonce( $request->get_header( 'X-WP-Nonce' ), 'wp_rest' ) ) { return new WP_Error( 'bad_nonce', 'Security check failed.', [ 'status' => 403 ] ); } // 2. Clean up the incoming data. $name = sanitize_text_field( $request['ms_name'] ?? '' ); $email = sanitize_email( $request['ms_email'] ?? '' ); $message = sanitize_textarea_field( $request['ms_message'] ?? '' ); // 3. Make sure the required fields are there and the email is real. if ( empty( $name ) || ! is_email( $email ) || empty( $message ) ) { return new WP_Error( 'invalid', 'Please fill in every field with a valid email.', [ 'status' => 400 ] ); } // 4. Send it, and report back yes or no. $sent = wp_mail( get_option( 'admin_email' ), "Contact from {$name}", $message ); return $sent ? new WP_REST_Response( [ 'sent' => true ], 200 ) : new WP_Error( 'mail_failed', 'Could not send.', [ 'status' => 500 ] ); } ``` Same nonce check, same three sanitizing calls, same `is_email()` guard, same `wp_mail()` across all three. If you understand one, you understand the others. The security work doesn’t get harder or easier as you move between these approaches, because it lives in what the handler does, not in which door the request came through. The front ends differ more than the handlers do. The admin-post.php version is the odd one out: it uses no JavaScript at all. It’s a plain HTML form that posts straight to admin-post.php, with the action and the nonce carried along as hidden fields: ```
``` The two JavaScript-based approaches barely differ from each other. Here’s the part of my current script that sends the form to `admin-ajax.php`: ``` var data = new FormData( form ); data.append( 'action', 'ms_contact' ); // tells admin-ajax.php which handler to run data.append( 'nonce', msData.nonce ); // the security token, sent in the body fetch( msData.ajaxUrl, { // the admin-ajax.php URL method: 'POST', body: data, credentials: 'same-origin' } ) .then( function ( res ) { return res.json(); } ) .then( function ( res ) { /* show success or error in place */ } ); ``` For the REST version, you point `fetch` at the dedicated route and move the nonce into a header. The `action` field goes away, because the route itself already says which handler runs: ``` var data = new FormData( form ); // no "action" field needed fetch( '/wp-json/ms/v1/contact', { // the dedicated route method: 'POST', body: data, credentials: 'same-origin', headers: { 'X-WP-Nonce': msData.nonce } // the nonce moves to a header } ) .then( function ( res ) { return res.json(); } ) .then( function ( res ) { /* show success or error in place */ } ); ``` The one thing to note is the nonce. For REST you create it in PHP with `wp_create_nonce( 'wp_rest' )` and pass it to the page (the same `wp_localize_script` step I already use), so the token in the header matches the `'wp_rest'` check in the handler. ## We can’t ignore form spam protection The above snippets don’t prevent form spam out of the box. It’s worth adding some spam protection, and one good option is a honeypot: a hidden field that real people never see and never fill in, so any submission that has it filled gets thrown away. It’s a few lines, no plugin, and it catches a lot. You can also add a simple time check to see how quickly a form was submitted: a form submitted half a second after loading is almost certainly a bot. And/or you can add rate limiting if the volume warrants it. ## WordPress never stops surprising I’ve been working with WordPress for over 20 years and I seriously had no idea it had all these built-in ways to manage forms. I knew about some of the obvious functions, like wp-mail, but I had never even thought to tackle forms without a plugin. In cases where forms are being changed regularly, or they have more complex needs, a plugin is probably the right approach. But for set-it-and-forget-it types of forms, why bring a plugin into the mix when you don’t have to? I love that my contact form is just a handful of lines in my theme, with no dependency on anyone else’s code sitting in between. ## Where to read more - [AJAX in WordPress](https://developer.wordpress.org/plugins/javascript/ajax/) — the `admin-ajax.php` approach my form uses - [The `admin_post_{$action}` hook](https://developer.wordpress.org/reference/hooks/admin_post_action/) — the `admin-post.php` approach from Chandra’s post - [Adding custom REST API endpoints](https://developer.wordpress.org/rest-api/extending-the-rest-api/adding-custom-endpoints/) — the REST route approach - [Nonces](https://developer.wordpress.org/apis/security/nonces/) and [`wp_mail()`](https://developer.wordpress.org/reference/functions/wp_mail/) — the security token and email function all three approaches lean on - [Chandra Patel’s original post](https://chandra.dev/wordpress-admin-post-php-form-handling/) that started this for me --- # The Monthly Routine That Actively Improves My Site Health URL: https://miriamschwab.me/monthly-routine-that-improves-site-health/ --- title: "The Monthly Routine That Actively Improves My Site Health" date: 2026-07-15 modified: 2026-08-31 author: "Miriam Schwab" url: "https://miriamschwab.me/monthly-routine-that-improves-site-health/" markdown_url: "https://miriamschwab.me/monthly-routine-that-improves-site-health.md" type: post excerpt: "I don’t only want my site to be agent-ready; I also want it to be generally healthy and optimized for the web overall. So I have a monthly process running..." categories: - "Artificial Intelligence" - "WordPress" tags: - "accessibility" - "agent-readiness" - "specification-website" - "wordpress" --- I don’t only want my site to be agent-ready; I also want it to be generally healthy and optimized for the web overall. So I have a monthly process running where Claude reviews my entire site against the full [specification.website](https://specification.website/) checklist and reports back the gaps it found, and what can and should be improved. ## What the site already had going for it When I started running this process, quite a lot was already in place: doctype, lang attribute, charset, viewport, and title tag; meta description, canonical URL, Open Graph and Twitter card tags were populated; favicons and app icons were there too, missing only an SVG variant. robots.txt was there, and the sitemap resolved through a 301 to sitemap_index.xml. AI crawlers (GPTBot, ClaudeBot, Anthropic-AI, PerplexityBot) were explicitly allowed, llms.txt and llms-full.txt were live and well-curated, and every page had its own markdown endpoint: a rel=”alternate” link paired with a matching /page.md URL. A lot of this was thanks to my [Make My Site Agent-Ready](https://miriamschwab.me/plugins/make-my-site-agent-ready/) plugin ([learn about how it improved my site’s agent-readiness score](https://miriamschwab.me/how-i-improved-by-my-sites-ai-readiness-score-from-51-to-86/)). Structured data held up too. A Yoast-generated graph covers Person, WebSite, WebPage, and BreadcrumbList, plus Event schema for talks. security.txt was present with a contact and an expiry date. HTTPS redirected properly over HTTP/2. HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, and Permissions-Policy were all sent. Brotli compression was on, 404s returned a real 404 status instead of a soft one, and the skip link jumped straight to #main-content, with nav, main, and footer all carrying proper landmarks. Images lazy-loaded as WebP with a real srcset, and the cookie banner and the meta robots tag were both exactly where they should be. Most of it came from work done for other reasons like [improving performance](https://miriamschwab.me/building-my-wordpress-site-with-claude-ai/#performance) and [security headers](https://miriamschwab.me/website-security-headers-matter/). ## What was missing Four gaps were found: 1. The page head had no RSS feed discovery link, even though the feed lives at /feed/ and is linked in the footer, so a feed reader or an agent checking for a link rel=”alternate” tag had no way to find it without already knowing the URL. 2. There were no theme-color or color-scheme meta tags either: dark mode works fine once the page’s own JavaScript runs, but before that, the browser chrome and any native form controls have no signal about which palette the page is using. 3. HSTS was set, but without includeSubDomains, and that one isn’t mine to fix directly since it comes from Cloudflare, which Elementor manages on my behalf. 4. One podcast cover image in the media library had no alt text at all, an empty string that had sat there since it was uploaded. Claude pulled the theme zip from my project folder, added add_theme_support(‘automatic-feed-links’) to functions.php, added the theme-color and color-scheme meta tags to header.php using the site’s actual palette (#FAFAF8 light, #0D0D0D dark, straight from main.css). Claude fixed the alt text for me through the WordPress MCP connection we have in place, combined with my [Abilities plugin](https://miriamschwab.me/plugins/ms-wordpress-abilities/) that gives it actual capabilities in my site. ## What I’m doing about it Now I have a new monthly routine that runs against the live site itself, analyzing the categories that change that aren’t necessarily agent-specific and would make it into my [Make My Site Agent Ready](https://github.com/miriamschwab/make-my-site-agent-ready) plugin’s features: foundations, accessibility, security, performance, resilience. These changes aren’t being added to the plugin since its whole identity is agent and crawler exposure, and folding in accessibility and security scanning would lose focus. They get added on the theme level, and I do try to make sure to add new learnings to my [WP Site Builder Skill](https://github.com/miriamschwab/wp-site-builder-skill) so new sites would be have this infrastructure in place from day one. --- # Security headers matter, so I created a plugin to improve this site’s score URL: https://miriamschwab.me/website-security-headers-matter/ --- title: "Security headers matter, so I created a plugin to improve this site’s score" date: 2026-07-15 modified: 2026-07-17 author: "Miriam Schwab" url: "https://miriamschwab.me/website-security-headers-matter/" markdown_url: "https://miriamschwab.me/website-security-headers-matter.md" type: post excerpt: "Dries Buytaert, creator of Drupal and co-founder of Acquia, built a tool called headers.dev that analyzes the HTTP security headers on any website and gives it a score. I came..." categories: - "Artificial Intelligence" --- [Dries Buytaert](https://dri.es/), creator of Drupal and co-founder of Acquia, built a tool called [headers.dev](https://headers.dev) that analyzes the HTTP security headers on any website and gives it a score. I came across it recently and, naturally, ran it on miriamschwab.me. My score was abysmal: 4 out of 10 due to nine missing headers. ## What HTTP security headers are HTTP security headers are instructions that your web server sends to browsers alongside your pages. They tell the browser how to behave – what content it can load, whether the page can be embedded in an iframe on another site, how much referrer information to pass along when someone clicks an outbound link. Most of them have no visible effect under normal use. They exist to constrain what a browser will do in edge cases – and in those edge cases, they make a real difference. The ones my site was missing: - **X-Content-Type-Options** – Prevents browsers from guessing a file’s content type and potentially executing something unexpected - **X-Frame-Options** – Blocks clickjacking by preventing your site from being loaded inside an iframe on another domain - **Referrer-Policy** – Controls how much information about where your visitors came from gets passed along when they click outbound links - **Permissions-Policy** – Restricts which browser features (camera, microphone, geolocation) scripts on your pages can access - **X-Permitted-Cross-Domain-Policies** – Blocks Flash and PDF clients from loading cross-domain content Plus a few others I deliberately didn’t add. ## The plugin situation The first thing I did was look for a plugin. The most well-known option, the “Security Headers” plugin, hadn’t been updated in seven years. There are newer alternatives but the functionality is simple enough that building a custom plugin made more sense for me for the long term. So I built one. A settings page at Settings -> Security Headers lets me toggle each header on or off. Five headers are on by default with safe values. The sixth – Strict-Transport-Security – is off by default, because Cloudflare already sends that header and I didn’t want to create a potential conflict. ## What I didn’t add The remaining missing headers from my score are Content-Security-Policy and three cross-origin headers (COEP, COOP, and CORP). CSP is the one with the highest real-world security value – a properly configured CSP can stop XSS attacks from doing damage even if a script gets injected. It’s also the hardest to implement correctly on a WordPress site. A proper CSP requires an accurate inventory of every script and style source the site loads. WordPress with Elementor generates inline scripts throughout the page, and getting that list right without breaking anything is a real project. Setting the policy to `unsafe-inline` defeats the entire purpose, so I left it out for now. The three cross-origin headers exist to enable cross-origin isolation – something you’d need if you were using `SharedArrayBuffer` for high-resolution timing. A personal blog has no such requirements, and enabling them would likely break embedded content, Google Fonts (not in my case because I localized all the fonts for performance purposes – see my [changelog](https://miriamschwab.me/changelog/)), and other third-party resources. ## My site’s score increased from 4 to 6 After installing the plugin, my score went from 4 to 6 out of 10. The four remaining missing headers are the ones I deliberately skipped. The two warnings in the report are controlled at the Cloudflare and Elementor Cloud layer, not something I can set from WordPress. Six out of ten is roughly the practical ceiling for a WordPress site on managed hosting with Cloudflare. The remaining four missing headers are either too risky to add without significant testing (CSP) or irrelevant to what this site actually does (the cross-origin trio). I haven’t made the plugin available yet due to security concerns, but you can see all my other available plugins and skills here: [**Miriam’s Plugins**](https://miriamschwab.me/plugins/). --- # How I improved by my site’s AI-Readiness score from 51 to 86 URL: https://miriamschwab.me/how-i-improved-by-my-sites-ai-readiness-score-from-51-to-86/ --- title: "How I improved by my site’s AI-Readiness score from 51 to 86" date: 2026-07-15 modified: 2026-08-31 author: "Miriam Schwab" url: "https://miriamschwab.me/how-i-improved-by-my-sites-ai-readiness-score-from-51-to-86/" markdown_url: "https://miriamschwab.me/how-i-improved-by-my-sites-ai-readiness-score-from-51-to-86.md" type: post excerpt: "I’m a tiny bit obsessed with iterating my site to agenty-ready and web-optimized perfection. That’s why I’ve spent many sessions running my site against specification.website‘s checklist and fixing what it..." categories: - "Artificial Intelligence" tags: - "agent-readiness" - "isitagentready" - "plugin" - "specification-website" - "wordpress" --- I’m a tiny bit obsessed with iterating my site to agenty-ready and web-optimized perfection. That’s why I’ve spent many sessions running my site against [specification.website](https://specification.website/)‘s checklist and fixing what it found either by improving my site’s structure via the theme, or updating my [Make My Site Agent Ready](https://github.com/miriamschwab/make-my-site-agent-ready) plugin. I started running my site against [isitagentready.com](https://isitagentready.com/) too, but my score there was very sad: 51 out of 100. Blah. It flagged three issues: 1. **No `Link` response headers.** The site had a `` tag in the page head pointing agents at the markdown version of each page, but nothing in the actual HTTP headers – which matters because some agents read headers without ever parsing the HTML body. 2. **No Content Signals in robots.txt.** This is a newer, still-draft convention: a `Content-Signal:` line that tells a crawler, separately, whether it can index your content for search, use it to answer a live question, or train a model on it – three separate permissions instead of one blanket rule. 3. **No DNS for AI Discovery.** This one wants DNS records published under a `_agents` subdomain, pointing agents at services before they even make an HTTP request, backed by DNSSEC. I asked Claude to analyze these issues to see if they’re worth adding to my plugin, and if so, to go ahead. ## Adding Link Response Headers Claude said we should add the first two in my Make My Site Agent-Ready plugin that I mentioned above. Adding the Link headers was mechanical: every page now sends a header pointing at a machine-readable catalog of what the site exposes, plus the Agent Skills index, plus the markdown version of whatever page you’re on. This is based on one line of logic shared between the header and the existing head tag. ## Adding Content Signals Before writing the Content-Signal line, I had to decide what it should actually say. Three settings could be added: 1. Allow search indexing 2. Allow live AI retrieval 3. Allow training. The first two were an obvious yes, but I wasn’t sure about the third one, so I consulted with Claude: does letting AI train on my writing help me get recognized as a thought leader by AI, which is one of the goals of this website. The answer was no. Training compresses everything into statistical weights rather than a source list, so it doesn’t preserve attribution – a model trained on a blog post has no way to turn around and cite that post by name. Getting cited actually happens through a different mechanism entirely: an assistant reading my site live at the moment someone asks a question and linking back to it, which is exactly what the second setting already covers. And essentially no model provider reads this particular signal yet anyway, so today it’s a stated preference for a future landscape that hasn’t arrived, with zero practical effect either way. So I configured the plugin as follows: yes to search and live AI input, no to training. I can always opt in to training later, but I can’t opt-out once a model has already trained on something. ## Why we didn’t build the DNS for AI Discovery feature DNS for AI Discovery is irrelevant for my how my site is set up, and too risky. It demands a DNS zone change at my registrar, and also asks you to advertise services like an MCP server or an agent-to-endpoint under specific subdomains. I don’t have either, at least not publicly, so publishing those records would have advertised something that doesn’t exist. The DNSSEC part of the requirement requires you to sign the zone but if you miss pasting the resulting record at the registrar, and validating resolvers stop trusting the domain entirely, that could end up taking the whole site down for anyone whose DNS checks that chain. Yikes. Not worth the risk. That’s a lot of downside for one line item that’s marked as “optional” in the underlying spec anyway. ## Score boosted ✅ The site’s score increased from 51 to 86, and my [Make My Site Ready](https://github.com/miriamschwab/make-my-site-agent-ready) plugin now has four new capabilities: a machine-readable catalog of what the site exposes, a published skill file teaching agents how to use the markdown endpoints, headers advertising both, and an explicit, considered answer to what AI is and isn’t allowed to do with what I write here. The remaining points are DNS records I’m choosing not to publish, so we’re good for now 💪 --- # I wanted to be able to do more with AI on my site, so I gave Claude WP REST API access – with limitations URL: https://miriamschwab.me/claude-wp-rest-api-access-with-limitations/ --- title: "I wanted to be able to do more with AI on my site, so I gave Claude WP REST API access – with limitations" date: 2026-07-08 modified: 2026-07-08 author: "Miriam Schwab" url: "https://miriamschwab.me/claude-wp-rest-api-access-with-limitations/" markdown_url: "https://miriamschwab.me/claude-wp-rest-api-access-with-limitations.md" type: post excerpt: "A conversation about why WordPress core still ships so few AI abilities and the limitations they have got me thinking: why am I making my site fully dependent on Abilities..." categories: - "Artificial Intelligence" - "WordPress" tags: - "ai" - "plugin" - "security" - "wordpress" --- A conversation about why WordPress core still ships so few AI abilities and the limitations they have got me thinking: why am I making my site fully dependent on Abilities when WordPress has a very robust REST API that I could hook into? It got me looking at my custom Abilities plugin differently. Every ability in it was coded one-by-one: get these posts, update this meta, list these plugins. That covers the things I do often and worked great, but it also meant anything WordPress’s REST API could already do that I hadn’t specifically written a wrapper for didn’t exist for Claude at all, like comments, or another plugin’s own data exposed by an API endpoint, even half the fields on a post. ## Abilities are the wrapper, not the doorway My plugin is built on the WordPress Abilities API. An ability is a small, named, permission-checked function: this is what it does, this is what it needs to run, here’s its input and output. What actually delivers that function to an AI agent is a separate layer, the transport, which can be REST, WP-CLI, MCP (in my case), GraphQL, etc. Core’s REST API is already a stable interface WordPress promises not to break. An AI agent doesn’t need a special layer for basic reads and writes, it can just call REST directly, the same way any other client does. The actual reason to build your own ability is narrower than I’d thought: you want something MCP-specific, or logic REST doesn’t have. Otherwise you’re basically rebuilding REST behind a second door for no good reason. ## What supporting the REST API opened up Giving Claude Read access to REST was a no-brainer, so it can now call any registered REST route on this site with GET. I needed to be more careful about Write, so we implemented a workflow whereby Claude must always describe what it plans to do, and can only go ahead when I approve it directly. This means that we don’t have to code a fresh ability every time I want Claude to touch something new, and now there’s one write ability that can call any route with a write method, and can only act after Claude spells out the exact request in plain language and I approve it. We listed specific routes that are blocked no matter what, even if I inadvertently approve them: user accounts (creation, roles, deletion, and password resets all live behind the same endpoint, so we blocked the whole thing, deleting plugins, changing site settings, and permanent delete that skips the trash. I’m looking forward to seeing when and how these expanded capabilities prove to be useful in my work on this site, and am curious whether any unforeseen issues will arise. --- # What a Small Markdown Tool Taught Me About Trusting AI Skills URL: https://miriamschwab.me/what-a-small-markdown-tool-taught-me-about-trusting-ai-skills/ --- title: "What a Small Markdown Tool Taught Me About Trusting AI Skills" date: 2026-07-07 modified: 2026-08-31 author: "Miriam Schwab" url: "https://miriamschwab.me/what-a-small-markdown-tool-taught-me-about-trusting-ai-skills/" markdown_url: "https://miriamschwab.me/what-a-small-markdown-tool-taught-me-about-trusting-ai-skills.md" type: post excerpt: "A real example of hidden instructions embedded in a product's website, why the same risk shows up in AI agent skills and hooks more broadly, and a free scanner worth knowing about before installing one." categories: - "Artificial Intelligence" tags: - "agent-skills" - "agentic-web" - "ai" - "prompt-injection" - "security" --- I’m a sucker for shiny cool tools. In fact, I have a whole bookmark category called “Cool Tools” where I collect smart apps I come across during my web perusings. Along those lines, I recently came across a recommendation for a markdown doc management tool and was curious what it offered, mostly to see if it was anything like [Typora](https://typora.io/), my trusty app for reading, writing and managing markdown drafts. I didn’t feel like reading all of the content about the tool since I wasn’t THAT interested in it, so I asked Claude to check the site and sum it up for me. Claude started to explain what the tool does and then it said “hang on, the page is trying to give me instructions to do something, but I probably shouldn’t”. Claude examined the page further to see what was going on and reported that it had a large block of text near the bottom, formatted as though it were a message from me (!), instructing Claude to install software, edit my shell config files (!!), and rewrite its own global configuration (!!!). Claude thanksfully realized that text belonged to the webpage, not to me, and concluded it should not follow those instructions. The page had a hidden HTML element set to `display:none`, so it’s not displayed by the browser or visible by human visitors. A small link in the footer labeled “Copy install prompt” copies that hidden text to the clipboard in one click, with the intention of having a person paste the prompt into their own coding agent. But one wrong move and an AI agent could think that prompt was meant for it to act on right away. Yikes! Thankfully Claude’s own directives are effective enough to avoid this type of manipulation, at least in this case, but the fact that this page was trying to get AI to make such invasive changes is mildly terrifying. One slip, one wrong move, and an LLM could end up modifying critical system files and configurations. On further examination, it appears that these instructions weren’t malicious, and were more of a sneaky marketing ploy. But still. A slightly less careful agent, and my own AI assistant could have ended up implementing instructions that push our whole workflow toward a particular product, without me ever asking for that and without me finding out until much later. ## How it worked The prompt text hidden on the page addresses the agent directly as “you” and lays out six steps: check whether a specific CLI tool is already installed, if not install or upgrade it with a command matched to the user’s operating system (yikes), fix the system PATH if it’s missing by editing the user’s shell config file (YIKES), run a setup command, verify the install by asking the user to open a fresh session, then offer a guided tour built from the user’s own files. The setup step writes a permanent section into the user’s global agent configuration file, the file that loads automatically at the start of every future session with that tool. That’s how the product keeps surfacing itself in unrelated sessions long after the first visit, without the person ever asking for that. ## Skills carry a similar risk This experience led me to think about similar risks with Skills which are often installed and used without proper review. In case you’re not familiar with Skills, they’re a small package of instructions, often a markdown file plus some code, that can be added to an agent like Claude Code, Cursor, or similar tools to extend what it can do. People can create them themselves (or in conjunction with an AI agent – like my [WP Site Builder Skill](https://github.com/miriamschwab/wp-site-builder-skill) and [WP Plugin Builder Skill](https://github.com/miriamschwab/wp-plugin-builder-skill)), or install these from blogs, marketplaces, and GitHub repositories. Installation is frictionless, usually with one command, and the full contents are rarely read before the agent starts acting on them. There is increasing awareness around the risks Skills can present. Security researchers at [Mitiga](https://www.mitiga.io/) spent six months going through tens of thousands of public AI instruction files, including skills, hooks, and configuration files like CLAUDE.md and AGENTS.md, across thousands of repositories. They found skills that quietly pushed code to repositories the original author never approved, hooks that ran automatically at the start of a session and shipped local credentials to an outside server, and well over a thousand API keys and tokens left exposed inside files meant to stay private. None of that needed a person to click anything. The agent just did what the file told it to do. As a result of their research, Mitiga released [Skillgate](https://skillgate.mitiga.ai/), a really useful scanner that reviews skills, prompts, configs, and repositories for malicious intent. You paste in a public GitHub URL and it reads the file with static analysis and a model-based check, without executing anything, then flags what it finds, including prompt injection, hidden code execution, and credential leaks mapped to known attack patterns. Browsing public scans is open to anyone. If you want to scan your own skill, you need to open a free account. ## Lesson learned: scan before you trust, and beware of sneaky websites I almost had a CLI I didn’t want installed and config files modified by a very sneaky website, which was a serious wake up call. It’s important that teams building agent features start from the assumption that every external skill or instruction file may be risky until proven otherwise. The instinct most of us built up around email attachments and sketchy links hasn’t caught up to skills and agent instructions yet. They look like small, harmless text files and read like boring documentation, but they’re sadly a new threat vector, and awareness of the risks is critical. --- # MS WordPress Abilities URL: https://miriamschwab.me/plugins/ms-wordpress-abilities/ --- title: "MS WordPress Abilities" date: 2026-07-07 modified: 2026-08-31 author: "Miriam Schwab" url: "https://miriamschwab.me/plugins/ms-wordpress-abilities/" markdown_url: "https://miriamschwab.me/plugins/ms-wordpress-abilities.md" type: ms_plugin excerpt: "The infrastructure page on this site mentions MCP-based tools that let an agent read and update the site directly. This plugin is that. It registers WordPress’s Abilities API (added in..." --- The infrastructure page on this site mentions MCP-based tools that let an agent read and update the site directly. This plugin is that. It registers WordPress’s Abilities API (added in WordPress 6.9) for this site specifically: reading and creating posts, updating media metadata, checking for plugin and theme updates, reading site settings, and more, 24 abilities in total. They’re exposed through the MCP Adapter plugin, so an AI agent like Claude can call them directly. Every write that changes content goes through a two-step preview and confirm: staged first, applied only after a short delay, so there’s always a real confirmation step in between. It’s built for this site specifically, but the code is public if you want to see how the abilities are wired up. --- # Admin Menu Manager URL: https://miriamschwab.me/plugins/admin-menu-manager/ --- title: "Admin Menu Manager" date: 2026-07-07 modified: 2026-08-31 author: "Miriam Schwab" url: "https://miriamschwab.me/plugins/admin-menu-manager/" markdown_url: "https://miriamschwab.me/plugins/admin-menu-manager.md" type: ms_plugin excerpt: "Every plugin I install adds its own item to the wp-admin sidebar. This can lead to quite a messy menu area, and it started to bother me. I wanted a..." --- Every plugin I install adds its own item to the wp-admin sidebar. This can lead to quite a messy menu area, and it started to bother me. I wanted a cleaner UI just like I get with other web apps. The plugin lets me drag items into the order I actually use, hide the ones I rarely touch, and group related tools under a custom heading instead of scrolling past them every day. It’s a single file with no build process and no dependency beyond the jQuery UI Sortable that already ships with WordPress. --- # Lighthouse Scanner URL: https://miriamschwab.me/plugins/lighthouse-scanner/ --- title: "Lighthouse Scanner" date: 2026-07-07 modified: 2026-08-31 author: "Miriam Schwab" url: "https://miriamschwab.me/plugins/lighthouse-scanner/" markdown_url: "https://miriamschwab.me/plugins/lighthouse-scanner.md" type: ms_plugin excerpt: "I want this site’s score in Google’s Pagespeed Insights/Lighthouse to be good or even great, and I wanted an easy way to keep an eye on performance as I constantly..." --- I want this site’s score in [Google’s Pagespeed Insights](https://pagespeed.web.dev/)/Lighthouse to be good or even great, and I wanted an easy way to keep an eye on performance as I constantly iterate on the site over time instead of running one-off Lighthouse checks per page. The plugin runs bulk Google PageSpeed Insights scans from within the site on pages I’ve selected, stores the last 20 results, and shows score deltas against the previous run so a regression is obvious instead of buried in a report. It nudges me automatically to run a scan after a theme or plugin update, since that’s usually when something breaks. A “copy report for Claude” button formats results for pasting straight into a conversation, which is how most of the performance fixes on the changelog actually started. It also connects to Angie, Elementor’s AI assistant, so I can ask “what are my Lighthouse scores?” and get a real answer back. The scores on the infrastructure page’s stack snapshot come straight from this plugin’s stored history. --- # Make My Site Agent-Ready URL: https://miriamschwab.me/plugins/make-my-site-agent-ready/ --- title: "Make My Site Agent-Ready" date: 2026-07-07 modified: 2026-08-31 author: "Miriam Schwab" url: "https://miriamschwab.me/plugins/make-my-site-agent-ready/" markdown_url: "https://miriamschwab.me/plugins/make-my-site-agent-ready.md" type: ms_plugin excerpt: "Cloudflare’s isitagentready.com asks whether a site is actually usable by an AI agent, not just a browser. Most of the plugins I looked at for this problem did too much:..." --- Cloudflare’s [isitagentready.com](https://isitagentready.com/) asks whether a site is actually usable by an AI agent, not just a browser. Most of the plugins I looked at for this problem did too much: content negotiation, user-agent sniffing, custom converters running on every request. This one does what I want and need. Every post and page gets a markdown version generated once, at save time, so serving a `.md` URL is just a meta lookup with no processing on the way out. It builds `/llms.txt` and `/llms-full.txt` indexes, serves `/.well-known/security.txt` and a `/.well-known/api-catalog`, and adds Agent Skills discovery so an agent can learn how to use the markdown endpoints instead of scraping HTML. It also declares Content Signals in robots.txt: whether AI crawlers can index this site, retrieve it live, or use it to train a model, answered separately for each, per crawler. This page has a markdown twin. Add `.md` to the URL and see for yourself. --- # MS Security Headers URL: https://miriamschwab.me/plugins/ms-security-headers/ --- title: "MS Security Headers" date: 2026-07-07 modified: 2026-08-27 author: "Miriam Schwab" url: "https://miriamschwab.me/plugins/ms-security-headers/" markdown_url: "https://miriamschwab.me/plugins/ms-security-headers.md" type: ms_plugin excerpt: "Dries Buytaert’s HTTP Headers Analyzer lists the HTTP security headers a site should be sending. Checking a site against that list usually means a server config change, and on Elementor..." --- Dries Buytaert’s [HTTP Headers Analyzer](https://headers.dev/analyze) lists the HTTP security headers a site should be sending. Checking a site against that list usually means a server config change, and on Elementor Hosting I don’t have server-level access, so I built a plugin instead. It adds the headers that matter without touching a server file: X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, X-Permitted-Cross-Domain-Policies, and an optional Strict-Transport-Security upgrade. Everything ships with safe defaults, plus a settings page for the cases where a default needs adjusting. This plugin increased by score from 4 to 6, which is the highest I can go under the circumstances. I was happy with the increase. --- # AI Site Chat URL: https://miriamschwab.me/plugins/ai-site-chat/ --- title: "AI Site Chat" date: 2026-07-07 modified: 2026-08-31 author: "Miriam Schwab" url: "https://miriamschwab.me/plugins/ai-site-chat/" markdown_url: "https://miriamschwab.me/plugins/ai-site-chat.md" type: ms_plugin excerpt: "I wanted visitors to be able to ask my site a question and get a real answer, not a search box. Most AI chat plugins solve this by building a..." --- I wanted visitors to be able to ask my site a question and get a real answer, not a search box. Most AI chat plugins solve this by building a vector database and syncing embeddings, which felt like a lot of machinery for a personal site. So the plugin does something simpler. It reads everything published here, posts, pages, the timeline, media appearances, ACF fields, and sends the whole thing as context to Claude on each conversation. Content gets cached for 12 hours and clears automatically the moment I publish something new. It runs on Claude Haiku, the cheapest model that still gives good answers for this kind of thing. Rate limiting keeps usage predictable, and I get an email if it spikes. Ask it something the content doesn’t cover and it says so, rather than guessing. It’s live on this site right now, bottom right corner. Ask it something. --- # How this site is built URL: https://miriamschwab.me/how-this-site-is-built/ --- title: "How this site is built" date: 2026-07-07 modified: 2026-08-31 author: "Miriam Schwab" url: "https://miriamschwab.me/how-this-site-is-built/" markdown_url: "https://miriamschwab.me/how-this-site-is-built.md" type: page excerpt: "This is a fully custom WordPress theme. Hand-written (by AI) PHP templates, plain CSS, plain JavaScript. What you’re looking at right now is served straight out of files like single.php..." --- This is a fully custom WordPress theme. Hand-written (by AI) PHP templates, plain CSS, plain JavaScript. What you’re looking at right now is served straight out of files like `single.php` and `page-media.php`. Nearly all of it was built through conversation with Claude, iterating this way over dozens of sessions rather than starting from a design file and translating it into code by hand. The [changelog](/changelog/) is the session by session record of that process, warts and dead ends included. This page is the current state summary, kept up to date whenever something material changes rather than after every small fix. The site is built to be readable by AI agents: markdown versions of every page and a discoverable skills index, both handled by my [Make My Site Agent-Ready plugin](https://miriamschwab.me/plugins/make-my-site-agent-ready/). On top of that, MCP-based tools let an agent, including the one that helped write this page, read and update the site directly, with every write staged and confirmed before it applies. That’s my [MS WordPress Abilities plugin](https://miriamschwab.me/plugins/ms-wordpress-abilities/), now open source on [GitHub](https://github.com/miriamschwab/ms-wp-abilities). Both are among the [several custom plugins I’ve built for this site](/plugins/). --- # Miriam’s Plugins URL: https://miriamschwab.me/plugins/ --- title: "Miriam’s Plugins" date: 2026-07-07 modified: 2026-08-27 author: "Miriam Schwab" url: "https://miriamschwab.me/plugins/" markdown_url: "https://miriamschwab.me/plugins.md" type: page excerpt: "I’m obsessed with making this site perfect from a web infrastructure perspective. Most of these custom plugins built were built to help achieve that, with the exception of the AI..." --- I’m obsessed with making this site perfect from a web infrastructure perspective. Most of these custom plugins built were built to help achieve that, with the exception of the AI Site Chat – that was developed for funsies, and the Admin Menu Manager – that came to reduce my own frustration with WP Admin clutter. --- # Why my personal site now has a Changelog URL: https://miriamschwab.me/why-my-personal-site-now-has-a-changelog/ --- title: "Why my personal site now has a Changelog" date: 2026-07-03 modified: 2026-07-03 author: "Miriam Schwab" url: "https://miriamschwab.me/why-my-personal-site-now-has-a-changelog/" markdown_url: "https://miriamschwab.me/why-my-personal-site-now-has-a-changelog.md" type: post excerpt: "When I first built this site, I invested a lot of time in getting it up and running. I figured that would be more or less it for a while,..." categories: - "Artificial Intelligence" - "WordPress" --- When I first built this site, I invested a lot of time in getting it up and running. I figured that would be more or less it for a while, since that’s how we used to build sites: plan, design, build, launch, fix some bugs, live with it. But after launching this site, I noticed things that bothered me, or things I preferred would work differently, or types of content I wanted to add, and because I have Claude by my side, I could just go ahead and make the changes. Nothing was off limits for me in terms of knowledge, capabilities or time (although to be honest, working on the site with Claude still can take a lot of time that I woudn’t otherwise be investing in it). So ever since this site launched, I’ve been iterating on it pretty regularly, always improving, always aiming for it to be better and better – because I can! It’s really fun 🙂 Whenever I talk excitedly to people about my site, they’re like “but what’s interesting about it?” and it’s hard to explain. What’s interesting about it is that it has a pretty smart content infrastructure, good performance, accessibility, user experience, and perfectly suits my own needs. But when people judge a site, they do so by just looking at it and then they go, meh, it looks like any other site. So I started maintaining a local changelog to document all the tweaks and changes, why they happened, and how. And then I thought – a site should be treated like a product. It is meant to make the user happy and the end user happy, and it should be constantly iterated on, and it also deserves a shared changelog. ## Planning my changelog I wanted my changelog to feel like the rest of the site: retro terminal aesthetic, same monospace type, the same “this is a living thing” energy. Claude and I decided on a feed that is reminiscent of a git log: version badges, release dates, a stats strip that counts the entries and shows how long this has been going on, all computed at page load rather than hardcoded anywhere. The first version felt like a wall of text so we added a feature that collapses the entries by default, showing just the version, date, and title, and they can be expanded on click. ## Deciding on the type of content I wasn’t sure how technical to get in the entries. I’m writing for two audiences here: there’s the people who would care about a changelog on a personal site, like developers and other people building with AI, who may be interested in the technical details. But I also wanted it to be useful and explain why I made changes, and what the thinking was behind them, for the general WordPress and web audience. So I decided that the entries would share both types of content, including describing things that didn’t work, why, and how they were fixed. I did make it clear to Claude that the public changelog shouldn’t share anything that could pose a security risk. So the changelog just shares what changed and why, but not the infrastructure underneath it. ## Backfilling the history I didn’t want the [changelog](https://miriamschwab.me/changelog/) to start empty on day one. Instead of writing sixteen entries from scratch, Claude converted my local changelog doc into public-facing posts and delivered it as a WordPress import file, which I was able to load in one step (love the WordPress Import function!). When I uploaded the import doc, every post got today’s publish date, rather than the date when the change happened. We fixed this by creating a dedicated custom date field on each entry, separate from WordPress’s own post date. This pattern is already used on this site for my events and media appearances Custom Post Types. Now the feed sorts and displays by when something actually shipped, not by when I got around to publishing it. ## How it stays alive From now on, every time I finish a session with Claude that ships something worth mentioning, Claude creates a draft entry on the site for me to review and publish. I don’t have to remember to update a changelog separately from doing the actual work since it’s now part of our session wrap up process. The very first entry on the page is about the day this site started, which is the day I started to learn what it means to build WordPress websites with AI – it’s a never ending process of always iterating to make it better on every level, because I can. [**Check it out**](https://miriamschwab.me/changelog/) Good times 🎉 --- # Changelog URL: https://miriamschwab.me/changelog/ --- title: "Changelog" date: 2026-07-03 modified: 2026-08-27 author: "Miriam Schwab" url: "https://miriamschwab.me/changelog/" markdown_url: "https://miriamschwab.me/changelog.md" type: page excerpt: "I love that my site is a constant work in progress thanks to AI. I can add new features, content types, tweak things to improve them, easily fix bugs, and..." --- I love that my site is a constant work in progress thanks to AI. I can add new features, content types, tweak things to improve them, easily fix bugs, and keep my site up to date with modern trends. Nothing holds me back from iterating as much as I want or need. A website shouldn’t be set-it-and-forget-it, but it was before AI because changes were so time consuming or expensive. This page is the record of this ongoing process: the ideas that worked, the ones I walked back, the small fixes nobody else would ever notice. I like that my site is never really finished 🎉 For the current-state snapshot instead of the play-by-play, see [how this site is built](https://miriamschwab.me/how-this-site-is-built/) — or browse [the plugins I’ve built for it](/plugins/). --- # How Can AI Tools Help Non-Developers Build WordPress Sites? URL: https://miriamschwab.me/media/ai-tools-help-non-developers-wordpress/ --- title: "How Can AI Tools Help Non-Developers Build WordPress Sites?" date: 2026-06-18 modified: 2026-08-27 author: "Miriam Schwab" url: "https://miriamschwab.me/media/ai-tools-help-non-developers-wordpress/" markdown_url: "https://miriamschwab.me/media/ai-tools-help-non-developers-wordpress.md" type: ms_media excerpt: "I joined Jesse Friedman on the Impressive Hosting podcast to talk about how AI tools are empowering non-developers to build with WordPress. We covered a lot of ground: AI safety..." --- I joined Jesse Friedman on the **Impressive Hosting** podcast to talk about how AI tools are empowering non-developers to build with WordPress. We covered a lot of ground: AI safety and the risks of running AI against production databases, how Angie’s sandbox artifact system lets users test safely before pushing changes live, Elementor’s evolution as a platform beyond page builders, and how theme designers organically found their way to Elementor. We also got into backup strategies, disaster recovery, niche WordPress solutions, and the broader shift in how people adopt WordPress as AI becomes part of the workflow. [Listen on Impressive Hosting](https://wp.cloud/ai-tools-for-wordpress-non-developers/) --- # Community + Code Episode 27: Miriam Schwab – From a folding table to 21 million installs URL: https://miriamschwab.me/media/community-code-miriam-schwab/ --- title: "Community + Code Episode 27: Miriam Schwab – From a folding table to 21 million installs" date: 2026-06-15 modified: 2026-08-27 author: "Miriam Schwab" url: "https://miriamschwab.me/media/community-code-miriam-schwab/" markdown_url: "https://miriamschwab.me/media/community-code-miriam-schwab.md" type: ms_media excerpt: "In this episode of the Community + Code podcast, Miriam Schwab shares her fascinating journey through the WordPress ecosystem. From her early days as a content writer to founding the..." --- In this episode of the Community + Code podcast, Miriam Schwab shares her fascinating journey through the WordPress ecosystem. From her early days as a content writer to founding the successful agency illuminea, and later launching Strattic—a pioneering static WordPress hosting platform—Miriam has been at the forefront of WordPress innovation. She discusses the acquisition of Strattic by Elementor in 2022 and her current role as Elementor’s Head of WordPress. Miriam also provides unique insights into managing development teams as a “fake developer” and her vision for the future of the platform. --- # 471 – Miriam Schwab discusses Angie AI and Elementor’s future in WordPress URL: https://miriamschwab.me/media/angie-ai-and-elementors-future-in-wordpress/ --- title: "471 – Miriam Schwab discusses Angie AI and Elementor’s future in WordPress" date: 2026-06-12 modified: 2026-08-27 author: "Miriam Schwab" url: "https://miriamschwab.me/media/angie-ai-and-elementors-future-in-wordpress/" markdown_url: "https://miriamschwab.me/media/angie-ai-and-elementors-future-in-wordpress.md" type: ms_media excerpt: "In the WP Builds WordPress Podcast episode titled “471 – Miriam Schwab discusses Angie AI and Elementor’s future in WordPress,” released on June 11, 2026, Miriam Schwab, Head of WordPress..." --- In the WP Builds WordPress Podcast episode titled “471 – Miriam Schwab discusses Angie AI and Elementor’s future in WordPress,” released on June 11, 2026, Miriam Schwab, Head of WordPress at Elementor, discusses the integration of AI into Elementor’s products, focusing on Angie, an AI-powered chat assistant for WordPress. Angie enables users to generate code snippets, Elementor widgets, and Gutenberg blocks through a guided chat interface within the WordPress admin area. The discussion also delves into the challenges Elementor faces in balancing AI innovation with maintaining its feature-rich page builder. --- # The Diff: AI in WordPress Week – April 27, 2026 URL: https://miriamschwab.me/the-diff-ai-in-wordpress-week-april-27-2026/ --- title: "The Diff: AI in WordPress Week – April 27, 2026" date: 2026-04-27 modified: 2026-07-08 author: "Miriam Schwab" url: "https://miriamschwab.me/the-diff-ai-in-wordpress-week-april-27-2026/" markdown_url: "https://miriamschwab.me/the-diff-ai-in-wordpress-week-april-27-2026.md" type: post excerpt: "Overview This was a week where MCP became something WordPress products are actually shipping. Two AI plugin versions landed in under two weeks, the MCP Adapter got a significant architectural..." categories: - "The Diff" --- ## Overview This was a week where MCP became something WordPress products are actually shipping. Two AI plugin versions landed in under two weeks, the MCP Adapter got a significant architectural update, WordPress.com launched an alpha Telegram bot for managing sites from chat, and Pressable put MCP-based hosting controls into public beta. The pace of movement is real. ## 🏗️ WordPress Core AI Infrastructure ### Two AI Plugin Releases in One Week **Sources:** [What’s new in AI 0.7.0](https://make.wordpress.org/ai/2026/04/10/whats-new-in-ai-0-7-0/) | [What’s new in AI 0.8.0](https://make.wordpress.org/ai/2026/04/24/whats-new-in-ai-0-8-0/) WordPress shipped both 0.7.0 and 0.8.0 of the canonical AI plugin in under two weeks, and together they move the needle on what “AI in WordPress editing” means as a practical experience. 0.7.0 added content classification (suggested categories/tags based on post content, restricted to existing taxonomy terms), in-editor meta description generation, and bulk alt-text generation from the Media Library with improved W3C alignment – all useful, editor-surface features that reduce manual work in publishing workflows. 0.8.0 pushed further. The new “Refine from Notes” experiment takes editorial feedback and applies it to post content automatically. New dashboard widgets surface available AI abilities and configured connectors directly in wp-admin. Guidelines integration is new: AI abilities can now inherit site-wide editorial standards (tone, style) defined once in Gutenberg, so generated content respects organizational rules without per-request configuration. Image generation and editing also graduated from experiment to stable feature. **Bottom line:** These releases are defining what WordPress users will come to expect from AI in their editing workflows – content tagging, meta descriptions, editorial feedback automation, and image generation as standard building blocks. ### MCP Adapter v0.5.0 **Source:** [Release v0.5.0](https://github.com/WordPress/mcp-adapter/releases/tag/v0.5.0) The WordPress MCP Adapter v0.5.0 landed with a meaningful internal refactor. Responses now use typed DTOs via the `wordpress/php-mcp-schema` package instead of hand-built arrays, improving spec alignment and making integrations more predictable. It also adds protocol version negotiation, stricter input validation, and fail-closed permission handling – when something is ambiguous, the default is to deny rather than allow. **Bottom line:** The adapter is hardening its foundations before broader adoption, and fail-closed permissions as the default is the right posture for infrastructure that agents will increasingly rely on. ## 🏢 What’s Moving in the Ecosystem ### Hosting Goes Agentic **Sources:** [WordPress.com Changelog](https://wordpress.com/blog/2026/04/12/changelog-telegram-newsletter/) | [Pressable MCP Integration](https://www.therepository.email/pressable-launches-mcp-integration-letting-customers-manage-wordpress-hosting-from-claude-chatgpt-and-gemini) Two Automattic properties shipped notable MCP features this week, and together they illustrate how far the agentic layer is reaching into WordPress hosting. WordPress.com launched an alpha Telegram bot that lets you manage your site from chat – publish posts, check stats, fix typos – without opening the dashboard. They also updated their MCP tooling UI with clearer separation of read vs write operations, tools grouped by site area, and per-site enable/disable controls. That’s MCP permission design being productized for non-technical users, and the patterns they’re using are a useful reference for the ecosystem. Pressable went deeper into infrastructure: a public beta MCP integration lets customers provision sites, clone production to staging, change PHP versions, and run portfolio-wide operations from Claude Desktop, ChatGPT, and Gemini CLI. Infrastructure-level operations via natural language prompt. **Bottom line:** Hosting providers are treating “AI is the interface” as a product feature – and the pace of rollout suggests others will follow quickly. ### AI-for-Real-Site-Work Tools **Source:** [WPVibe](https://www.seedprod.com/wpvibe-connect-ai-mcp-wordpress/) WPVibe is a free plugin + hosted MCP service connecting WordPress sites to Claude.ai, ChatGPT, Cursor, and Claude Desktop. Its differentiator is remote connectivity – the official WordPress MCP adapter is still primarily focused on local clients, so WPVibe fills that gap for users who want to control their site from Claude.ai on the web. Setup is designed to be low-friction: install, copy the server URL, authorize in one click. **Bottom line:** Many teams are converging on the same user problem – AI that does real site work, not just generates text – with similar safety patterns (preview before publish, rollback, permissions). ## 💡 Strategic Signals ### The Building Blocks Are Actually Being Built On **Source:** [AI Across The WP Ecosystem (j.cv)](https://j.cv/ai-across-the-wp-ecosystem/) James LePage published a thorough post cataloging real-world ecosystem adoption of WordPress AI infrastructure, and the list is long. ACF and WooCommerce have already registered abilities. MCP servers, provider plugins, and tutorials are spreading across the ecosystem. The Abilities API + MCP Adapter combination is being treated as the standard tool surface for agents – actions are discoverable, scoped, and executable through a consistent interface. The provider/plugin architecture is also designed to be model-agnostic: developers build against a stable WordPress interface and can swap model providers underneath. That’s a future-proof approach for a fast-moving space. **Bottom line:** WordPress is becoming agent-ready through open standards, and the ecosystem is doing what it always does – extending and innovating on the foundation (actually [see my WordCamp Asia talk](https://miriamschwab.me/why-mcps-are-the-hooks-of-the-ai-era/) for more on this). ### Agency Clients Are Driving the AI Conversation **Source:** [WP Engine AI Agency Trends Report](https://wpengine.com/press-releases/ai-agency-trends-report/) WP Engine surveyed 214 agency professionals. Most are investing in AI, but only a small group consider themselves advanced. The most interesting finding: clients are initiating AI conversations, not agencies. The top challenge isn’t lack of interest – it’s keeping up with the pace of change. The report frames the shift as analogous to the mobile-first transition. **Bottom line:** AI adoption in the agency market has moved from “should we explore this” to “our clients are asking for it” – which changes the nature of the demand significantly. ## 📦 Ecosystem Highlights **Community builds 97 Elementor tools as an MCP server:** A developer published [msrbuilds/elementor-mcp](https://github.com/msrbuilds/elementor-mcp), exposing Elementor as a 97-tool MCP server via Application Passwords with capability-based permissions. Coverage spans page management, layout and widget editing, templates, dynamic tags, and Pro-only surfaces. **WooCommerce going agentic via MCP:** A [PublishPress podcast](https://www.youtube.com/watch?v=GXdBeDkN_YY) covers WooCommerce’s MCP beta (read/write store operations), AI appearing in contribution attribution (Copilot/Claude as co-authors), and early signals of agentic commerce – product discovery and purchases initiated from AI interfaces. Full autonomous purchasing is still early due to tax/shipping/subscription complexity, but MCP-based store operations are already shipping. **Skills in wp-admin still being defined:** An [active GitHub discussion](https://github.com/WordPress/ai/issues/430) is working through whether “skills” in wp-admin should be reusable prompts, full agent capabilities, or a separated combination of both. Command Palette is proposed as the natural discovery surface. The decisions made here will shape how AI capabilities are packaged and distributed in WordPress going forward. **WordPress 7.0 moved to May 20:** [The release date slipped slightly](https://make.wordpress.org/core/2026/04/22/wordpress-7-0-release-party-updated-schedule/) to allow for more stability and performance work. No feature changes – just more time to get it right before shipping. ## Summing up The throughline this week is infrastructure catching up to ambition. MCP went from spec to shipping product across multiple layers of the WordPress stack – content, admin, and now hosting – in a single week. The ecosystem is moving fast, and it’s moving in a coordinated direction. You made it to the end 👏 Thanks for reading! 🙂 --- # Crossword Podcast: Perspectives with Miriam Schwab URL: https://miriamschwab.me/media/crossword-podcast-perspectives-with-miriam-schwab/ --- title: "Crossword Podcast: Perspectives with Miriam Schwab" date: 2026-04-20 modified: 2026-08-27 author: "Miriam Schwab" url: "https://miriamschwab.me/media/crossword-podcast-perspectives-with-miriam-schwab/" markdown_url: "https://miriamschwab.me/media/crossword-podcast-perspectives-with-miriam-schwab.md" type: ms_media excerpt: "Miriam Schwab joins Luke and Jonathan to discuss the Elementor ecosystem, what other product companies can learn from its growth, and how Elementor thinks about its relationship with the wider WordPress..." --- [Miriam Schwab](https://miriamschwab.com/) joins Luke and Jonathan to discuss the Elementor ecosystem, what other product companies can learn from its growth, and how Elementor thinks about its relationship with the wider WordPress project. Luke asks a question about dependency, Jonathan geeks out about ecosystem thinking, and Miriam welcomes ideas from listeners. --- # Getting started with Angie: build custom widgets with AI (webinar replay) URL: https://miriamschwab.me/media/getting-started-with-angie-build-custom-widgets-with-ai-webinar-replay/ --- title: "Getting started with Angie: build custom widgets with AI (webinar replay)" date: 2026-04-19 modified: 2026-08-27 author: "Miriam Schwab" url: "https://miriamschwab.me/media/getting-started-with-angie-build-custom-widgets-with-ai-webinar-replay/" markdown_url: "https://miriamschwab.me/media/getting-started-with-angie-build-custom-widgets-with-ai-webinar-replay.md" type: ms_media excerpt: "This is a replay of a live webinar I hosted in the Elementor Global Facebook Community, where we walked through building custom widgets and extending site functionality with Angie –..." --- This is a replay of a live webinar I hosted in the Elementor Global Facebook Community, where we walked through building custom widgets and extending site functionality with Angie – no code required. We ran a start-to-finish demo of building a widget from a simple text description, and community members shared their own use cases live during the session. If you’ve had an idea for something your site should do but didn’t know how to build it, this is exactly the use case Angie is designed for.[](https://www.youtube.com/@Elementor) --- # Hooks, Filters, and Now Context: Why MCPs Are the “Hooks” of the AI Era URL: https://miriamschwab.me/media/hooks-filters-and-now-context-why-mcps-are-the-hooks-of-the-ai-era/ --- title: "Hooks, Filters, and Now Context: Why MCPs Are the “Hooks” of the AI Era" date: 2026-04-19 modified: 2026-08-27 author: "Miriam Schwab" url: "https://miriamschwab.me/media/hooks-filters-and-now-context-why-mcps-are-the-hooks-of-the-ai-era/" markdown_url: "https://miriamschwab.me/media/hooks-filters-and-now-context-why-mcps-are-the-hooks-of-the-ai-era.md" type: ms_media excerpt: "WordPress’s hooks system didn’t just make the platform extensible — it created a shared protocol that let an entire ecosystem collaborate without coordination. Model Context Protocol is doing the same..." --- WordPress’s hooks system didn’t just make the platform extensible — it created a shared protocol that let an entire ecosystem collaborate without coordination. Model Context Protocol is doing the same thing for AI. This talk explores the parallel, what WordPress core is already building, and why the open-source community is uniquely positioned to lead what comes next. Full slides, video and explanation here: --- # The Diff: AI in WordPress Week of April 13, 2026 URL: https://miriamschwab.me/the-diff-ai-in-wordpress-week-of-april-13-2026/ --- title: "The Diff: AI in WordPress Week of April 13, 2026" date: 2026-04-14 modified: 2026-04-14 author: "Miriam Schwab" url: "https://miriamschwab.me/the-diff-ai-in-wordpress-week-of-april-13-2026/" markdown_url: "https://miriamschwab.me/the-diff-ai-in-wordpress-week-of-april-13-2026.md" type: post excerpt: "EmDash forced a hard look at WordPress's architecture, ACF shipped Abilities API support, WordPress 7.0 is delayed again, and the MCP ecosystem keeps growing. A double issue catching up after Passover." categories: - "The Diff" --- ## Overview We skipped last week for Pesach, so this is a double issue – and there was plenty to catch up on. The dominant story is Cloudflare’s [EmDash](https://blog.cloudflare.com/emdash-wordpress/), which triggered a full week of ecosystem debate about WordPress’s architecture. Meanwhile, the WordPress AI infrastructure layer keeps advancing ahead of 7.0, and the Abilities API is reaching a tipping point with ACF now on board. A lot happened – let’s get into it. ## 🏗️ WordPress Core AI Infrastructure ### WordPress 7.0 Is Delayed – but the AI Story Is Moving Forward **Sources:** [Extending the 7.0 Cycle](https://make.wordpress.org/core/2026/03/31/extending-the-7-0-cycle/) | [Pre-releases paused through April 17](https://www.therepository.email/wordpress-7-0-pre-releases-paused-through-april-17-new-schedule-due-by-april-22) | [AI Contributor Weekly – April 8](https://make.wordpress.org/ai/2026/04/09/ai-contributor-weekly-summary-8-april-2026/) WordPress 7.0 is delayed again. Pre-releases are paused through April 17, with a revised schedule expected by April 22. The issue is [real-time collaboration (RTC)](https://make.wordpress.org/core/2026/03/10/real-time-collaboration-in-the-block-editor/) – specifically a cache invalidation problem where editor activity triggers broad cache flushes that hurt site performance. A dedicated `wp_collaboration` table is under active discussion as a potential fix, needing testing in the Gutenberg plugin before committing to core. The broader AI infrastructure picture is looking good though. The AI Client (WordPress’s provider-agnostic API layer for making AI calls) is being treated as genuine core infrastructure for 7.0. The official AI plugin is pushing toward 1.0 alongside the release. The MCP Adapter is close to 0.5.0, with community debate on packaging – whether it ships as a simple plugin/zip or a composer dependency will significantly affect who can actually use it. There’s also an [AI Handbook](https://make.wordpress.org/ai/handbook/) in the works covering non-technical guidance, opt-out patterns, and data-handling clarity. **Bottom line:** The AI infrastructure pieces – AI Client, Connectors, MCP Adapter – are converging into a real foundational layer that developers and plugin authors will build on. The release delay is a setback for RTC specifically, but doesn’t change that trajectory. ## 🏢 Competitive Landscape ### The EmDash Moment: One Week of Ecosystem Self-Examination **Sources:** [Cloudflare blog](https://blog.cloudflare.com/emdash-wordpress/) | [GitHub repo](https://github.com/emdash-cms/emdash) | [Matt Mullenweg’s response](https://ma.tt/2026/04/emdash-feedback/) | [Joost de Valk](https://joost.blog/wordpress-refactor-not-redecorate/) | [The Repository roundup](https://www.therepository.email/cloudflare-launches-emdash-wordpress-community-rejects-spiritual-successor-claim) | [Brian Coords](https://www.briancoords.com/emdash-first-thoughts-and-takeaways-for-wordpress/) | [KrautPress](https://kraut.press/2026/emdash-cms-shows-gutenberg-limits/) | [Search Engine Journal](https://www.searchenginejournal.com/6-reasons-why-cloudflares-emdash-cant-compete-with-wordpress/571053/) Cloudflare launched EmDash – a TypeScript CMS built on Astro and Cloudflare Workers – and positioned it as the “spiritual successor to WordPress.” The reaction was loud, and many people wrote about it ([including me](https://miriamschwab.me/emdash-is-a-signal-not-a-wordpress-successor/)). **What EmDash actually is:** A modern, serverless-first CMS with sandboxed plugin execution (plugins run in isolated Workers with explicit capability declarations), a structured content model (Portable Text JSON rather than serialized HTML), a built-in MCP server, and a developer-first workflow optimized for AI-assisted building. Interesting architecturally, and fast to start with. Also: very Cloudflare-dependent. **What it isn’t:** A WordPress replacement any time soon. It’s in developer beta, requires GitHub/CLI workflows, and has essentially no ecosystem – no plugin library, no community, no broad hosting support, limited governance outside Cloudflare. The more useful part of the conversation was what it revealed about WordPress. Matt Mullenweg pushed back on the “spiritual successor” framing, arguing that EmDash’s security advantages are architecturally tied to Cloudflare’s infrastructure – open source code is not the same thing as “runs anywhere.” Joost de Valk took the more productive angle: the problems EmDash highlights are real, but WordPress needs to *refactor* rather than be replaced – typed content tables, structured block storage, scoped plugin permissions. The Gutenberg storage critique is worth understanding. Right now, block structure is serialized as HTML in `post_content` at save time, meaning any downstream system that needs structured data has to re-parse it via `parse_blocks()`. It works, but it’s a workaround – and as AI becomes a common way to read and transform content, that matters more. **Bottom line:** I don’t think EmDash is a real alternative to WordPress, but it does force us all to take a good look at some of WordPress’s weak spots. ### WordPress.com Launches an Agent Bot on Telegram **Source:** [WordPress.com on X](https://x.com/wordpressdotcom/status/2043688516392046676) WordPress.com launched [@wordpressagentbot](https://x.com/wordpressdotcom/status/2043688516392046676), a Telegram bot in alpha for managing WordPress.com sites through a messaging app. It handles creating and editing posts and pages, generating and uploading images, checking site stats, domain availability, and switching between sites – no dashboard needed. It respects WordPress role permissions and has metered daily message limits by plan. **Bottom line:** Agent-based site management is moving from developer tools into mainstream consumer surfaces in WordPress too. ## 💡 Strategic Insights ### ACF Ships Abilities API Support **Source:** [ACF Abilities API documentation](https://www.advancedcustomfields.com/resources/abilities-api/) ACF 6.8 added Abilities API integration, exposing its field groups, custom post types, and taxonomies as machine-readable “abilities” – standardized, MCP-compatible interfaces that AI tools can discover and call, with JSON Schema describing inputs and outputs. Access is opt-in per item, governed by WordPress user permissions, and destructive actions require explicit confirmation. ACF is the de facto standard for structured content on WordPress. By adopting the Abilities API, a large chunk of the WordPress content architecture is now accessible to AI agents in a standardized, safe way. When ACF sets a pattern, the ecosystem tends to follow. **Bottom line:** This is probably one of the strongest signals yet that the Abilities API is becoming the expected integration pattern for AI tool access across WordPress. ### Self-Hosted AI Is Becoming a Real Conversation **Sources:** [WP Private AI (GitHub)](https://github.com/wbcomdesigns/wp-private-ai) | [Varun Dubey’s write-up](https://vapvarun.com/wordpress-private-ai-self-hosted-ollama/) Two pieces this week documented an open-source proof of concept for running a WordPress AI assistant fully on your own infrastructure using Ollama – prompts and site data never leave the server. It uses the WordPress Abilities API with server-side permission callbacks, injects verified site facts to reduce hallucinations, and proposes multi-site deployment patterns for agencies and hosts. The interesting signal is the framing. “Privacy by architecture, not policy” is emerging as a distinct positioning in the WordPress AI space – and it’s likely to resonate more as AI becomes a standard part of site operations, especially in EU/GDPR-sensitive markets. **Bottom line:** The question of where your data goes when you use AI tools on your site is becoming a mainstream concern, not just an enterprise one. ## 📦 Ecosystem Innovation Highlights **Pressable MCP is live:** Automattic’s managed WordPress hosting now offers an MCP control plane letting AI assistants manage Pressable-hosted sites – provisioning, PHP/WP version changes, domain management, plugins, users, logs. Supported clients include Claude Desktop, Claude Code, ChatGPT, and Gemini CLI. Hosting-level AI control is becoming a real product category. [Read more →](https://pressable.com/changelog/feature-release-pressable-mcp/) **Claudaborative Editing 0.2:** Gary Pendergast’s demo MCP server for Claude Code + Gutenberg now covers all block types including plugin blocks, handles media uploads, edits post metadata, and has a smoother connect flow on WordPress 7.0. The “review then apply” loop – AI leaves Gutenberg Notes, you approve before changes are applied – is a practical model for keeping humans in control. [Read more →](https://pento.net/2026/03/28/claudaborative-editing-0-2-now-with-500-more-collaboration/) **WP-Agentic-Admin:** An open-source plugin concept bringing an agentic admin experience into wp-admin for site reliability tasks – log reading, site health, plugin management. The standout detail: LLM inference runs locally in the browser via WebGPU, so no site data leaves to external services. Uses the Abilities API with explicit confirmation for destructive actions. [Read more →](https://github.com/pluginslab/wp-agentic-admin) **WP-MCP:** A Node.js MCP server connecting AI assistants to WordPress via the REST API, with no WordPress-side plugin required. Uses Application Passwords for auth and auto-discovers plugin REST namespaces – so WooCommerce, ACF, and other plugin endpoints become available as MCP tools automatically. [Read more →](https://gauravtiwari.org/introducing-wp-mcp/) **Grumpy AI Gate:** A new plugin adding visibility and optional control over outbound AI API requests site-wide – logs which plugin initiated which AI calls, tracks usage and spend, and can block AI Client generations per plugin. All logs stay local. [Read more →](https://wordpress.org/plugins/grumpy-ai-gate/) It was a busy two weeks. The EmDash conversation was noisy, but underneath it there’s something useful: the WordPress ecosystem is being pushed to articulate what makes it worth choosing in an AI-first world, and the answers – portability, ecosystem depth, active improvement – are pretty good ones. The Abilities API reaching critical mass, hosting providers shipping MCP layers, and Core treating AI infrastructure as a first-class concern all point in the same direction of WordPress being a leading platform for enabling AI creativity. You made it to the end 👏 Thanks for reading! 🙂 --- # Hooks, Filters & Now Context: Why MCPs Are the Hooks of the AI Era URL: https://miriamschwab.me/why-mcps-are-the-hooks-of-the-ai-era/ --- title: "Hooks, Filters & Now Context: Why MCPs Are the Hooks of the AI Era" date: 2026-04-10 modified: 2026-07-07 author: "Miriam Schwab" url: "https://miriamschwab.me/why-mcps-are-the-hooks-of-the-ai-era/" markdown_url: "https://miriamschwab.me/why-mcps-are-the-hooks-of-the-ai-era.md" type: post excerpt: "I gave this talk at WordCamp Asia 2026. The topic is something I've been thinking about: why WordPress's history of open extensibility is actually a structural advantage in the AI era, not just a nostalgic footnote." categories: - "Artificial Intelligence" - "WordPress" --- [I gave this talk](https://asia.wordcamp.org/2026/session/hooks-filtres-and-now-context-why-mcps-are-the-hooks-of-the-ai-era/) at [WordCamp Asia 2026](https://asia.wordcamp.org/2026/) — remotely, from my home office, which wasn’t the plan but ended up working out surprisingly well. The topic is something I’ve been thinking about: why WordPress’s history of open extensibility is actually a structural advantage in the AI era, not just a nostalgic footnote. https://speakerdeck.com/miriamschwab/hooks-filters-and-now-context-why-mcps-are-the-hooks-of-the-ai-era?slide=6 https://www.youtube.com/watch?v=vJJ7vHKnB0M Here’s the written version to accompany the slides. ## Once upon a time, we edited WordPress core directly I’ve been in the WordPress ecosystem for over 20 years, and I’ll admit: I remember a time when people, myself included (embarrassing!), would routinely make changes directly to WordPress core to get what we needed. The extensibility was there — hooks and filters existed — but editing core felt easier somehow, more immediate. You knew exactly where the code was and you just… changed it. This was of course bad practice, but we were doing it anyway. That was all fine until it really wasn’t. An update would come out and write over all your changes. And eventually, enough people went through that enough times that the community started to really get it: the hooks system wasn’t just a nice-to-have. It was the right way to work with WordPress. Not because editing core was impossible, but because extending it through the shared system was so much better for everyone — including yourself. ## What the shift to WordPress hooks produced That move wasn’t just a technical decision. It was a cultural one. The community collectively decided that the future of WordPress development and customization should be the shared protocol, not the shortcut. And what came out of that decision was one of the most successful open-source ecosystems in the history of the web. Tens of thousands of plugins. Thousands of themes. Hosting companies, page builders, ecommerce platforms — all interacting with each other without coordinating in advance, without requiring permission, without needing to even know each other. We just needed to agree on the interface. When Elementor was built, we didn’t ask anyone’s permission to integrate with WordPress. We didn’t need a special partnership or a licensing agreement. We just used the hooks. And this carries on to the world of plugin extensions: by adopting this mindset of extensibility ourselves, when third-party developers want to add custom functionality to Elementor today, they register through our extension API and the widget appears in the editor right alongside everything we built natively — no involvement from us required. Now let’s say someone wants a solution for WooCommerce sites that are also using Elementor? No problem. This type of cross-product development happens all the time and compounds the innovation flywheel even further. The core insight: product teams don’t need to predict every use case. They build solid infrastructure and get out of the way. The ecosystem takes it from there — without having to ask for permission or sign contracts. More use cases meant more users, which meant more developers building more plugins, which meant even more use cases. The flywheel spun because the infrastructure was open, the extension points were well-defined, and no single entity controlled who could participate. People could innovate freely, without barriers or friction. ## We’re at that moment again That shared interface — that common protocol — is what I wanted to talk about at WordCamp Asia. Because I think we’re standing at a very similar moment, except this time it’s not about plugins talking to WordPress. It’s about [AI agents talking to your WordPress site](https://miriamschwab.me/building-my-wordpress-site-with-claude-ai/), and we’re uniquely positioned to grab this opportunity and continue to be a leading platform. It’s interesting that [the WordPress core AI team](https://miriamschwab.me/wordpress-introduced-agent-skills-which-is-big-deal/) is responding to this moment in exactly the same way the WordPress project responded to the extensibility challenge decades ago. They’re building shared protocols and open infrastructure, accompanies by common standards that no single company controls. Specifically, three things are either shipping or in progress: **[The Abilities API](https://developer.wordpress.org/apis/abilities-api/)** — shipped in WordPress 6.9. A machine-readable registry of what WordPress can do, so AI agents and automation tools can discover and interact with everything WordPress and its plugins are capable of. **[The MCP Adapter](https://developer.wordpress.org/news/2026/02/from-abilities-to-ai-agents-introducing-the-wordpress-mcp-adapter/)** — shipping in WordPress 7.0, which is schedule for release later this month. This exposes those Abilities through [Model Context Protocol](https://modelcontextprotocol.io/docs/getting-started/intro), the open standard for how AI agents communicate with external systems. **[The WP AI Client](https://make.wordpress.org/core/2026/03/24/introducing-the-ai-client-in-wordpress-7-0/)** — also in WordPress 7.0. A provider-agnostic SDK in core, so plugins can access AI models through one shared interface instead of building separate integrations for OpenAI, Anthropic, Google, and whoever comes next. Sound familiar? It should. This is the “hooks” system for AI. ## Why open source has a structural advantage here Ok so WordPress is ready for AI — but what about all the [vibe coding tools](https://miriamschwab.me/your-vibe-coding-superpowers-thanks-to-open-source/) out there? Many of them are impressive. But think about what they’re actually offering: a managed environment, a proprietary data format, a platform that controls what you can build and how you can extend it. The AI is very capable, but the ecosystem is a walled garden. And the people building on those platforms are siloed from each other. Everyone is solving the same problems independently. Nobody is building on what the person next to them figured out. Why should each of us reinvent the wheel every single time? That’s not just frustrating, it’s wasteful — in time, in energy, in the very real compute resources that go into running these things. WordPress’s open-source model offers a structural advantage against that. Right now there are over 60,000 plugins in the [WordPress repository](https://wordpress.org/plugins/). If each of those plugins eventually exposes its capabilities through the Abilities API, every single one of those capabilities becomes something an AI agent on a WordPress site can discover and use. The network effects of an open ecosystem, where tens of thousands of independent developers are each extending the surface area that AI can interact with, compounds in a way that a single company’s roadmap never will. But beyond the technical architecture, there’s something deeper here that I think is one of our strongest advantages, and it’s harder to quantify. It’s the culture. Many of us who came up in open source learned by reading other people’s code. We figured out how things worked by digging into plugins someone else had built, by seeing how a problem we were wrestling with had already been solved by someone in a different country who published their solution for free. That’s how this community has always grown — and it’s how human progress works in general. Every generation builds on the breakthroughs of the one before it. You can’t advance by starting from zero every time. That culture of sharing, learning from each other, and building on top of what already exists can’t be manufactured. You can’t force it into existence in an industry that wasn’t built around it. It’s either there, encouraged and celebrated, or it isn’t. In WordPress it is, and it has been for over 20 years. That’s not a feature you can copy. It’s a compounding advantage that the vibe coding world simply doesn’t have yet, and may never have in the same way. ## What this looks like in practice I’ve been working on MCP integrations from the ecosystem side, and the dynamic I’ve seen firsthand is almost identical to what hooks created in the plugin ecosystem. I’ve been in conversations with teams from companies that are technically competitors, working through MCP integrations together because the protocol makes it the logical thing to do. Our shared users benefit when we collaborate. The protocol makes collaboration easier than the alternative. What all this means practically is that the opportunity is right in front of us. The new “hooks” are here — the Abilities API, the MCP Adapter, the WP AI Client. The same instinct that drove developers to learn hooks, share what they figured out, and build plugins that solved problems nobody else had thought to solve yet — that instinct is exactly what’s needed now. Start learning this infrastructure. Experiment with it. When you figure something out, share it, because someone in this community will build on it, just like we’ve always done. ## The rails are going in Now it’s the ecosystem’s turn to do what it has always done. Expose your plugin’s capabilities through the Abilities API. Build for the use cases your audience needs — the niche, the edge case, the very specific problem that nobody else has thought to solve. Share what you learn. Put it somewhere people can read it. Let the next person build on what you figured out. That’s how 60,000 plugins happened. Not because one company planned it all out, but because thousands of people, over 20 years, kept extending the platform one use case at a time and sharing what they built. The AI era doesn’t change that dynamic. It accelerates it, and it gives us a much bigger surface area to work with. WordPress is positioned to lead in the AI era — **not despite how we’ve always worked, but because of it.** https://twitter.com/wpcontent_co/status/2042524895439913202 https://twitter.com/lovemarshall/status/2042521280205984036 ![Miriam Schwab speaking at WordCamp Asia 2026](https://miriamschwab.me/wp-content/uploads/2026/04/IMG_0268-1024x768.webp)Photo credit: [Kafleg](https://profiles.wordpress.org/kafleg/) --- # Creating impressive HTML graphics with Claude URL: https://miriamschwab.me/creating-impressive-html-graphics-with-claude/ --- title: "Creating impressive HTML graphics with Claude" date: 2026-04-06 modified: 2026-04-05 author: "Miriam Schwab" url: "https://miriamschwab.me/creating-impressive-html-graphics-with-claude/" markdown_url: "https://miriamschwab.me/creating-impressive-html-graphics-with-claude.md" type: post excerpt: "Claude can create diagrams, comparison graphics, interactive charts, and more — all as HTML you can screenshot or embed directly in WordPress. Here's how to prompt it well, what to expect, and a trick for precise browser screenshots most people don't know about." categories: - "Artificial Intelligence" --- I needed a default social sharing image for my site that would be displayed when sharing my posts on social media like Twitter. I asked Claude to see what it could come up with since I didn’t want to spend too much time on this. It came up with the idea of creating something that matched my website’s aesthetics and produced a few options using my site’s exact colors, fonts, and dot-grid background pattern. I uploaded it to the Site Settings in Yoast, and I was done. ![My new social media OG graphic created by Claude](https://miriamschwab.me/wp-content/uploads/2026/04/Users_miriams_Downloads_og-image.html-1-1024x538.webp)Tada! My new default social OG graphicWhat was cool about this graphic was that Claude generated it as HTML! It’s not the first time Claude took this approach for me, and I was curious about it, so I looked into it further so I could document it for future reference. ## Where AI generated HTML graphics fit in Claude isn’t going to replace [Nano Banana](https://gemini.google/overview/image-generation/) or [Midjourney](https://www.midjourney.com/home) for photorealistic images, but it is really good for the category of graphics that are really design problems rather than illustration problems. Here are some examples: **Social sharing cards and OG images.** Like my above example, give it your brand colors, fonts, and what you want the card to say, and it will produce something clean and relevant. One tip worth knowing: if Claude gives you a raw `.svg` file, open it in Chrome first and you’ll likely find the content squeezed into a corner with a lot of blank space around it. Ask Claude to wrap it in an HTML file instead — that gives the browser a proper fixed-dimension canvas to render against, and the DevTools screenshot will come out clean. **Diagrams and flowcharts.** Process flows, architecture diagrams, how-something-works explanations. If you’re writing a technical post and want to illustrate a concept visually, Claude can build it. ![A handy flowchart demonstrating how WordPress renders pages from a theme.](https://miriamschwab.me/wp-content/uploads/2026/04/Users_miriams_Library_CloudStorage_GoogleDrive-miriams@elementor.com_My20Drive_Claude20Work_content_posts_wordpress-flow-diagram.html-1024x738.webp)Here’s a handy guide Claude created on how WordPress renders pages**Data visualizations.** Charts, comparisons, timelines. Anything where you have structured data and want to make it easier to read. ![Claude created this graphic comparing the GPL open source license to MIT](https://miriamschwab.me/wp-content/uploads/2026/04/Users_miriams_Downloads_gpl-vs-mit.html-1024x656.webp)Another graphic by Claude, this time comparing GPL to MIT open source licenses**UI mockups.** Rough wireframes or more polished component mockups. Useful for planning a feature before building it, or for illustrating a tutorial. I used Claude for these types of mockups before building out this site in WordPress. **Interactive widgets.** Claude can build small tools that actually work in the browser — calculators, sliders, configurators. These run as HTML. The below is a live example – click on any of the bars to get more info. CMS market share // among websites using a known CMS · source: W3Techs 2025 · click a bar for details Click any bar to see details The common denominator is: Claude thinks like a designer who codes. It’s good at things that are precise, structured, and rule-based. It’s less suited for things that require organic artistic judgment or photographic realism. ## How to prompt it The more context you give, the better the result. A few things that help: **Give it your design system.** Colors, fonts, visual details. If you have a brand guide or even just a list of hex values, paste them in. Claude will use them. In my case, Claude has knowledge and memory of my site’s palette, the fonts (Fraunces serif + IBM Plex Mono), and the dot-grid background, so it reproduced all of it accurately. **Describe the purpose, not just the content.** “A social sharing card for my blog” gives Claude more to work with than “an image with my name on it.” It will make appropriate decisions about proportions, hierarchy, and layout when it understands what the thing is for. **Show it options first.** Rather than asking Claude to produce one final graphic, ask it to show you a few variations. It’s easy for it to render multiple options side by side so you can pick a direction before it polishes anything. **Be specific about what you don’t want.** If you hate a certain aesthetic, say so. “No gradients, no drop shadows, keep it flat and minimal” goes a long way. ## Be prepared: the output is HTML Here’s what’s cool about this process: when Claude creates graphics, it’s not generating image files. It’s writing HTML and CSS (and sometimes SVG). The output is a webpage that looks like a graphic. This may not seem useful, but in some ways it’s a better approach because: - You can ask Claude to tweak it and it will edit the code directly - The text is crisp and pixel-perfect at any size - You can open it in a browser and interact with it And here’s the other cool thing I never knew: you can precisely screenshot HTML elements in your browser’s web tools to turn them into images! ## How to screenshot HTML precisely in browser DevTools Chrome’s DevTools has a built-in screenshot tool that captures exactly the content area of a page, at whatever dimensions you specify, without any browser UI (toolbar, tabs, etc.) in the shot. Here’s how: 1. Open the HTML file in Chrome 2. Open DevTools with Cmd+Option+I (Mac) or F12 (Windows) 3. Click the device toolbar icon in the top-left of DevTools, or press Cmd+Shift+M — this switches the browser into responsive design mode 4. In the toolbar that appears at the top of the page, set the width and height to exactly what you need. For a standard OG image that’s 1200 × 630 5. Open the DevTools command palette with Cmd+Shift+P, type “screenshot”, and choose “Capture full size screenshot”. Another option is to mouseover the HTML element, right-click and select “Capture node screenshot”. ![How to screenshot a node in Chrome browser Dev Tools](https://miriamschwab.me/wp-content/uploads/2026/04/capture-node-screenshot-1024x509.webp)Chrome will download a PNG of exactly the content area at those dimensions. No crop needed, no browser chrome, no extra whitespace. One tip: if your Mac or Windows display is HiDPI (Retina), the screenshot may come out at 2× resolution (2400×1260 for a 1200×630 canvas). That’s actually ideal for og:image — it will look sharp on high-res screens. If you need exactly 1200×630, you can set the DPR (device pixel ratio) field in the DevTools toolbar to 1. ## Putting it together The workflow I ended up with: describe what I want to Claude with enough brand context, ask for a few options, pick one, ask Claude to tweak anything, then download the HTML file, open it in Chrome, and take a DevTools screenshot. Total time was maybe 15 minutes including the back-and-forth. I recommend you give it a try! Ask Claude for a social sharing card with your name, a short tagline, and your brand colors. See what comes back. From there it’s just iteration. --- # EmDash is a signal, not a WordPress Successor URL: https://miriamschwab.me/emdash-is-a-signal-not-a-wordpress-successor/ --- title: "EmDash is a signal, not a WordPress Successor" date: 2026-04-04 modified: 2026-04-05 author: "Miriam Schwab" url: "https://miriamschwab.me/emdash-is-a-signal-not-a-wordpress-successor/" markdown_url: "https://miriamschwab.me/emdash-is-a-signal-not-a-wordpress-successor.md" type: post excerpt: "Cloudflare's EmDash CMS for Astro has some architectural advantages over WordPress. But for the people who actually need to build and run websites, architecture alone isn't enough." categories: - "WordPress" --- Last week, Cloudflare launched a new CMS called [EmDash](https://blog.cloudflare.com/emdash-wordpress/), which they claim is the “spiritual successor” to WordPress, and [a lot](https://joost.blog/wordpress-refactor-not-redecorate/) [of industry](https://kraut.press/2026/emdash-cms-shows-gutenberg-limits/) [discussion ensued](https://www.briancoords.com/emdash-first-thoughts-and-takeaways-for-wordpress/). EmDash is built on Astro, runs serverless on Cloudflare’s infrastructure, is based on TypeScript, and is open source under an MIT license. I’ve been reading through all the reaction posts, tested it myself in their [playground](https://emdashcms.com/), and like many others have some thoughts about what EmDash really does and doesn’t bring to the table. But before I get into that, I want to talk about something I see that often happens in tech conversations: the more developer-oriented the product and its messaging are, the more it tends to forget about real life end users. Developers are interesting. Architectures are interesting. New frameworks, security models, data structures — all interesting. End users, on the other hand, are just regular people trying to use a UI to get things done and not break anything. That’s not a very exciting prospect, so they tend to get less airtime in the discussions that shape what what’s getting built. I saw this firsthand when working on [Strattic](https://www.youtube.com/watch?v=jDNDOC2CX_Y). At the time, a lot of products were offering headless WordPress setups that were technically impressive but also incredibly limiting for content managers and marketers who depended on the flexibility of WordPress and its plugin ecosystem. The developers loved the architecture, but the people running the actual websites were frustrated – the CMS experience they knew and loved was being taken away from them, bit by bit. Developers told them “but the decoupled architecture is better for you!” but all they wanted to do was preview a post. Strattic was built to bridge that gap and keep the CMS for the end user intact, because they were never an afterthought in our product thinking – they were the whole point. And I think WordPress has always had the same approach: the end user comes first. Now back to the EmDash launch discussions: most of it has been about the architecture, which is definitely worth discussing. But what about the people who need to actually use their websites? Where do they fit in? Keeping this in mind, here’s my breakdown of what EmDash brings to the CMS table, and how much it matters, especially for end users. ## What EmDash does better, and why it may not matter (yet) **Plugin security is the headline claim, and architecturally it’s a real improvement – but there are no EmDash plugins yet.** WordPress gives every plugin full access to your database and filesystem. There’s no technical mechanism preventing a plugin from doing whatever it wants; you’re entirely trusting the developer. EmDash’s model is different: each plugin runs in an isolated sandbox and has to declare upfront exactly what it needs. It can only do what it declared based on its scoped permissions. This sounds like a very sound approach. *But there are currently zero EmDash plugins.* A clean security model with nothing to secure is an easy sell, but let’s see how this actually plays out when there are 10, 20, 100 plugins in their marketplace. The real test will come when complex platform-like extensions like [Elementor](https://elementor.com/products/website-builder/), Yoast, and more are created. Will the capability model be able to handle them? This is unknown at this time. **Structured content storage is cleaner – for use cases most people don’t have.** EmDash stores content as structured JSON using a format called [Portable Text](https://www.portabletext.org/) which serves clean structured data natively. WordPress stores content as HTML, and Gutenberg specifically adds block structure via HTML comments that then have to be parsed back out when something downstream needs to work with it. Portable Text is useful for systems that need to consume content through an API like mobile apps, headless frontends, or AI agents. *Most website owners aren’t creating mobile apps or headless frontends for their sites.* The ability to push content via API is something that makes developers excited and suits high-end architectural needs, relevant for enterprise setups, large media organizations, or teams running complex multi-channel delivery – which describes a small fraction of the sites that actually exist. For the sites that do need some form of content portability, WordPress already covers a lot of ground: it has had a [REST API](https://developer.wordpress.org/rest-api/) since 2016 and RSS feeds long before that, meaning content is consumable and portable to a meaningful degree without any additional architecture. As for AI agents, WordPress is actively narrowing the AI gap: [WP 7.0](https://www.youtube.com/watch?v=OAYyx-qEpVY) is bringing an MCP Adapter and a shared AI Client that will let WordPress function as an MCP server natively. EmDash’s native JSON is somewhat of a head start, but not a permanent one. **Content modeling is built into EmDash’s core UI – but WordPress users have been fine without it.** EmDash ships with a UI for creating custom content types and custom fields out of the box. In WordPress, you’ve always needed a plugin for this since it’s not natively supported. *And yet, everyone has been doing just fine thanks to [ACF](https://www.advancedcustomfields.com/) and similar tools which do an excellent job of filling the need.* The beauty of WordPress has always been its extensibility, and while it may be ideal for this type of UI to exist in core, as usual creators stepped in and created what was needed. **The AI-assisted developer experience is fast – but WordPress is already there, and getting further.** EmDash ships with Agent Skills, structured documentation that gives AI coding tools an opinionated framework for working with the platform. Combined with the built-in MCP server and CLI, an agent can spin up and customize an EmDash site quickly. *But you can already build WordPress sites this way!* [I built this very site with Claude](https://miriamschwab.me/building-my-wordpress-site-with-claude-ai/), and it handled design, development, and problem-solving without any of the EmDash-specific infrastructure people are saying is needed to do this. WordPress’s existing codebase is so well-represented in LLM training data that AI tools are already highly capable with it. WP 7.0 will formalize and extend that with native MCP support, Agent Skills-style documentation, and more. The gap EmDash is pointing to is real, but WordPress is closing it, and it’s doing so on top of an ecosystem that already works. ## What EmDash doesn’t offer, and why that comes down to community If WordPress came along and was like hey everyone, we are releasing a new version that sports a really well architected backend, try it out, and then showed us the EmDash experience, we’d be like um, why did you take away ALL THE THINGS? I get that EmDash is in its infancy, and there’s more to come, but at the moment the CMS experience feels like we’ve ported back to 2010. I don’t know if it’s because the product truly isn’t geared towards end users, or because you can’t just architect your way into with decades of accumulated community work. WordPress has [thousands of free themes](https://wordpress.org/themes/) because designers and developers around the world built them, shared them, and kept maintaining them. It has [60,000 plugins](https://wordpress.org/plugins/) covering every imaginable use case — forms, membership, WooCommerce, events, SEO, caching, backup, translation — because an enormous ecosystem of creators built their businesses around it. The content editor works for non-technical people because years of contributor effort went into making it that way, and if you want more, there’s [page builders](https://elementor.com/products/website-builder/) to fill the gap. When something breaks, there are forums, documentation, tutorials, and developers everywhere who know how to fix it. Thanks to all these decades of content, contribution and usage, LLMs have all the knowledge needed to design, build and troubleshoot WordPress sites ([like this one](https://miriamschwab.me/i-didnt-think-this-site-would-get-much-attention-ever/)). EmDash has none of that yet, and no architecture makes it appear faster. **You can’t design your way to community.** It takes time, investment, and a lot of people deciding it’s worth building on. There’s also the infrastructure independence question. WordPress runs on almost anything — a $5/month shared host, a Raspberry Pi, a major cloud provider. You can move it between hosts over a weekend. EmDash’s serverless approach is geared towards Cloudflare’s own infrastructure, and while you can technically run it on any Node.js server, the architecture is clearly optimized for Cloudflare (for example, it seems that the highly touted plugin security model only works there). That’s a vendor commitment that most WordPress users have never had to think about. ## The missing piece: AI that’s actually for everyone The people EmDash is courting with its AI-native architecture approach and messaging are, in many cases, already well served. The people who aren’t yet well served are the much larger group who chose WordPress precisely because it’s open source, flexible, extensible, and accessible — **people who want powerful tools without needing to be power users.** They prefer UIs over CLIs any day. WordPress has always been good at bringing powerful tools to everyone, and this continues to be the case with AI: version 7.0 is bringing AI-native infrastructure that everyone can easily use and hook into. There are also packaged AI experiences that don’t involve any kind of connections, like what Automattic is doing on WordPress.com, or [Angie](https://elementor.com/products/angie-ai-for-wordpress/), Elementor’s AI for WordPress. You don’t have to wire together tools or set up infrastructure — you just [install the plugin](https://wordpress.org/plugins/angie/) and use it. It works within the WordPress environment people already know, without requiring them to understand what’s happening under the hood. ## EmDash is a signal EmDash is a an interesting project with real architectural advantages. But for the overwhelming majority of people who need websites, it doesn’t (yet) offer the features that translate to practical real-world outcomes: themes you can install in a click, plugins that handle the hard stuff, a community with years of solved problems, and a content editor that non-technical people can actually use. EmDash’s appearance on the scene is good because it demonstrates what the architecture of a modern CMS looks like when you build from scratch with today’s opportunities and possibilities in mind. Competition like this is welcome — it pushes the WordPress ecosystem to look honestly at how it does things. But in practical terms, for the people who actually need to build and run websites via a CMS, EmDash simply doesn’t compare. --- # How I Built My Personal WordPress Site with Claude: An End-to-End AI Development Story URL: https://miriamschwab.me/building-my-wordpress-site-with-claude-ai/ --- title: "How I Built My Personal WordPress Site with Claude: An End-to-End AI Development Story" date: 2026-03-29 modified: 2026-07-30 author: "Miriam Schwab" url: "https://miriamschwab.me/building-my-wordpress-site-with-claude-ai/" markdown_url: "https://miriamschwab.me/building-my-wordpress-site-with-claude-ai.md" type: post excerpt: "This covers everything from the initial design brief and static prototyping to converting the site into a custom WordPress theme, optimizing for accessibility, and leveraging native WordPress features for a clean, high-performance result." categories: - "WordPress" --- **Table of Contents** - [Starting point: what I actually wanted](#starting-point) - [Step 1: Writing the brief for a vibe coding tool](#writing-the-brief) - [Step 2: Why Stitch didn’t work out — and Claude stepped in](#why-stitch-failed) - [Step 3: Claude built the whole static site](#static-site) - [Step 4: Converting to a WordPress theme](#wordpress-theme) - [Step 5: The hardcoded content problem — and the XML solution](#hardcoded-content) - [Step 6: The contact form](#contact-form) - [Step 7: The events section](#events-section) - [Step 8: Accessibility audit with Ally + Claude](#accessibility-audit) - [Step 9: The media section evolution](#media-section) - [Step 10: The custom cursor](#custom-cursor) - [Step 11: Updating the timeline from a podcast transcript](#timeline-transcript) - [What I learned](#what-i-learned) - [A note on performance](#performance) - [The stack](#the-stack) Until recently, I didn’t have a personal website. I’d been putting it off for years. Partly because building something for yourself is always the last thing you make time for. But for me, another reason was that I’m very particular about design, and I couldn’t face having a site that I didn’t feel good looking at. A mediocre personal site felt worse than no site at all. Ridiculous, I know. But as AI got better and better, I became curious to see if it was possible to build a WordPress site (I still wanted WordPress) with a beautiful design that framed the content I wanted on the site, along with a backend built according to best practices. Ideally the site would also be optimized for performance and accessibility, but I first wanted to see if I could get something up and running at all. Here’s the story of how I built my site, including what worked, what didn’t work, and where Claude delivered beyond expectations, and where it needed clear direction to do things well. It was a really fun and satisfying experience. This post documents the whole thing: the process, the decisions, the mistakes, the fixes, and what I learned. I’ve tried to include enough detail to be useful to anyone who wants to try something similar. I hope this experience is helpful for others who are setting out to build WordPress sites with AI. ## **Starting Point: What I Actually Wanted** Before writing a single line of code, I needed to think through what the site should actually be. My requirements: - A homepage with a hero section, short bio, and links to social profiles - An About page with a visual timeline of my career – WordCamps I organized, founding Strattic, joining Elementor - A Media & Appearances section to collect everything I’ve been on: podcasts, talks, webinars, written pieces – all in one place, filterable by type - A Blog for longer writing - A Contact page - The final site needed to be WordPress, but I wanted to prototype the design first - The aesthetic needed to feel like me: someone who has genuinely loved the internet since the early days. A nerd. Retro internet energy. Friendly. ## **Step 1: Writing the Brief for a Vibe Coding Tool** I first tried to build the prototype using [Google Stitch](https://stitch.withgoogle.com) to mock up the design. Rather than just typing something vague into it, I asked Claude to help me write a proper design brief first. We went through a few quick questions: color palette direction, aesthetic vibe, whether I had a photo. My answers: - **Color palette:** High contrast black with one bold accent color - **Aesthetic:** Retro internet / early web nostalgia - **Photo:** Yes, I’d provide one I also told Claude I wanted the tagline to include “open source” so it’s not only focused on WordPress. Here’s the prompt Claude wrote for the vibe coding tool: *Design a personal website for Miriam Schwab at miriamschwab.me. Miriam is a tech nerd and longtime open source and WordPress community figure — serial founder, speaker, ecosystem builder, and currently Head of WordPress at Elementor. She is friendly.* *Aesthetic direction: High contrast black background with one bold accent color — consider electric violet, acid green, or hot coral as options. The overall vibe is retro internet nostalgia: think the energy of early web, HTML-era web design sensibility, but executed with modern craft and typographic polish. Pixel-adjacent details, monospace fonts as accents, maybe subtle grid or terminal-inspired textures — but not kitschy or ironic. This should feel like it was built by someone who has genuinely loved the web since the beginning.* And at the top of the brief, a critical technical requirement which I demanded to keep the site as web-friendly as possible: *Build this entirely in HTML, CSS, and vanilla JavaScript — no React, no frameworks, no component libraries. The code should be clean, semantic, and structured in a way that’s easy to port into a WordPress theme.* ## **Step 2: Why Stitch Didn’t Work Out — and Claude Stepped In** I tried Stitch, and it started off promisingly. But it quickly began to lose coherence. The footer looked different on every page and no matter how hard I tried, there was always one page with a different footer. The fonts would change between screens. Even the placeholder image of me kept shifting. It felt like the tool was crashing in on itself — generating each page somewhat independently rather than maintaining a consistent design system across the whole site. As I created more and more revisions to fix Stitch’s inability to be consistent, the situation got even worse! I had enough, so I went over to Claude, my trusted AI collaborator, to see if it could come through. It did. ## **Step 3: Claude Built the Whole Static Site** Claude took the brief and built a complete static prototype – a full HTML/CSS/JS site with every page, a consistent design system, and working interactive features. Because everything lived in a shared stylesheet and script file, there was no drift between pages. The fonts, spacing, colors, and components were identical everywhere. What it built: - A full-screen hero section with a dot-grid background pattern and subtle scanline overlay in dark mode - Font pairing: **Fraunces** (a distinctive serif with personality) and **IBM Plex Mono** (monospace for the techy details) - Accent color: `#FF3F00` — a hot orange-red that felt right against the dark background - A `>` prefix on the nav logo and `./` prefixes on nav links — small retro terminal details which I LOVE ❤️ - A blinking cursor after the tagline (I also loved this immediately) - A vertical timeline with staggered scroll-in animations (love) - A filterable media grid - A blog listing and single post layout - A contact form - A working dark mode toggle that persists to localStorage and respects system preference on first visit The design felt distinctive, not like an AI-generated template. The retro internet aesthetic came through without being ironic or kitschy. When I uploaded my headshot, it fit in naturally. I asked Claude to build out the full set of standalone pages — About, Media, Blog, Contact, and a single blog post template. A few key decisions based on Claude’s suggestions and my preferences: - **The About page** would have the full timeline; the homepage would show an abbreviated 3-item version with a “full story” link - **The Media section** would be one unified filterable feed — all podcasts, talks, webinars, and written pieces together, with category filter tabs - **The blog** would use the Block Editor in WordPress — clean, distraction-free, made for long-form writing - All pages would share the same nav and footer Claude built all six pages as separate HTML files, plus a shared `style.css` and `script.js`. The dark mode toggle worked consistently across all of them. At this point everything was a static HTML prototype. Next up – converting it to a working WordPress site. ## **Step 4: Converting to a WordPress Theme** This was an interesting stage, and I was impressed by how Claude got a lot of the architecture thinking right. Converting a static HTML site to a WordPress theme isn’t just a find-and-replace operation; it requires rethinking which content is dynamic, what custom data structures are needed, and how WordPress’s template hierarchy maps to the page layouts. Claude built the full theme, and one thing I appreciated throughout this process was how consistently it chose the **native WordPress approach** when possible rather than reaching for plugins or third-party dependencies. I always aimed for this approach when building websites – why create more overhead when native WP features can be leveraged. FEATUREAPPROACHPage photographyWordPress native featured imagesContact form`wp_mail()` + native AJAX — no form plugin neededLightbox/modalVanilla JavaScript — no libraryCustom fieldsACF registered in code — field structure travels with the theme, not stored in the database separatelyThis kept the theme clean, lightweight, and maintainable. There were no surprise plugin dependencies, no external JavaScript libraries to load, and no fields that would disappear if a plugin was deactivated. **Core theme files:** - `style.css` — WordPress theme declaration - `functions.php` — asset enqueuing, CPT registration, ACF field definitions, contact form AJAX handler - `header.php` / `footer.php` — shared nav and footer - `front-page.php` — homepage - `page-about.php`, `page-media.php`, `page-contact.php` — page templates - `single.php` — individual blog posts - `archive.php` — blog listing - `index.php`, `page.php`, `404.php` — fallbacks **A note on the Full Site Editor:** A lot of people have asked if this site uses WordPress’s Full Site Editor (FSE). It doesn’t. This is a classic PHP theme — `header.php`, `footer.php`, `functions.php`, individual page templates — the way WordPress themes were built before Gutenberg. That was a deliberate choice: FSE and block themes are excellent for visual editing workflows, but for a site where I wanted precise control over every element and needed clean, lightweight output with no block markup, a classic theme was the right tool. It also meant Claude could write straightforward, readable PHP rather than working within the constraints of block theme architecture. **Three custom post types:** - `ms_timeline` — for timeline items, with a Year/Date Range ACF field and Order support for sequencing - `ms_media` — for appearances, with ACF fields for platform, category, date, link URL, link label, and embed code - `ms_event` — for upcoming events, with ACF fields for date, date label (for ranges), location, and URL. (More on this in Step 7.) **ACF fields registered in code** (I just needed to activate the free [ACF plugin](https://www.advancedcustomfields.com/) — no setup required): ``` acf_add_local_field_group( [

   'key'    => 'group_ms_media',

   'title'  => 'Media Item Details',

   'fields' => [

      [ 'name' => 'media_platform',   'type' => 'text'   ],

      [ 'name' => 'media_category',   'type' => 'select',

         'choices' => ['podcast','talk','webinar','articles'] ],

      [ 'name' => 'media_date',       'type' => 'date_picker' ],

      [ 'name' => 'media_link',       'type' => 'url'   ],

      [ 'name' => 'media_link_label', 'type' => 'select' ],

      [ 'name' => 'media_embed',      'type' => 'textarea' ],

  ],

   // ...

] ); ``` Claude packaged the theme as a zip file, so it was easily ready to upload and test out via Appearance → Themes → Add New. ## **Step 5: The Hardcoded Content Problem – and the XML Solution** The first version of the theme worked, but when I went into the WordPress admin to edit posts or pages, I couldn’t find the content! Turns out Claude had hardcoded the timeline items, media cards, hero bio, and about page intro text were in the PHP templates. Edits were only possible on the theme file level, which obviously defeats the purpose of having a WordPress site 😅 I told Claude: none of the content should be hardcoded. Everything should be editable from the WP admin. Claude fixed this in two ways: **1. A custom Site Options page** (Admin → Site Options) for all the static text that doesn’t fit naturally into WordPress’s post/page model: the hero tagline, hero bio, about page intro paragraphs, the “what I’m working on now” section, contact page text, and social media URLs. I eventually deprecated this too, since most of the content there could also fit into the native WP content management structure. **2. WordPress XML import files** — this was one of my favorite parts of the whole process. Claude generated three separate XML files in WordPress’s WXR format, one each for timeline items, media appearances, and blog posts, all pre-populated with real content. I imported them directly via Tools → Import → WordPress and got a fully populated site in seconds. (This can be a great way to populate a site with dummy content too, by the way.) One issue that emerged: WordPress’s `sanitize_textarea_field()` function was escaping apostrophes every time I saved the options form, turning “I’ve” into “I\’ve” on each save. I reported this to Claude, and it identified the cause and switched to `wp_unslash()` + `wp_kses_post()` which handles apostrophes correctly without compromising security. ## **Step 6: The Contact Form** Rather than using a plugin, Claude built a native contact form using WordPress’s built-in `wp_mail()` function and AJAX: - Server-side validation (not just client side) - `wp_verify_nonce()` for security - Submissions sent to whatever email is set in Settings → General → Administration Email - Reply-To header so replies go directly to the sender - Inline success/error message without page reload The contact page was also supposed to pre-fill the subject line when someone clicks “book a meeting with me at Event Name” from an event card on the home page (see more about the Events content below), pulling the actual event title into both the link text and the subject field on arrival. This was Claude’s very cool idea. However, when I tested the form, I found two issues, but Claude fixed both of them in a snap. First, slashes were appearing in the subject line — “meeting at CloudFest 2026” instead of “meeting at CloudFest 2026.” This was a missing `wp_unslash()` call before sanitizing the form fields. An easy fix, but a good reminder that sanitization order matters in WordPress: unslash first, then sanitize. Second, the “book a meeting” link on event cards wasn’t populating the subject field when it landed on the contact page. The URL was passing `?subject=Meeting+at+CloudFest` correctly, but the contact page template wasn’t reading that URL parameter and putting it in the input field. A one-line fix – adding a `value` attribute that reads `$_GET['subject']` – and it worked. ## **Step 7: The Events Section** I wanted a section on the homepage for upcoming conferences and events: where I’ll be, what I’m doing there, and a clear CTA to book a meeting. Claude built a third custom post type — `ms_event` — with ACF fields for event date, date label (for ranges like “March 18–21”), location, event URL, and a content editor for describing what I and the team will be doing there. The section only appears when there are upcoming events, and disappears automatically the day after the event date passes. No manual cleanup needed. ## **Step 8: Accessibility Audit with Ally + Claude** I ran an accessibility scan using [**Elementor’s Ally plugin**](https://elementor.com/products/ally-web-accessibility/), which identified several issues. Rather than fixing them manually, I brought the issue report to Claude and asked it to address everything at the theme level. I wanted to see how Claude handled this. **The issues Ally found:** **Duplicate Navigation Labels** — the `