All field notes
202510 min read

Amender: editing the final WordPress page

I built Amender for a common WordPress problem. A finished page can combine output from WordPress itself, a theme, blocks, a page builder and several plugins, and the element somebody wants to change may not have an obvious source file. Amender edits that combined result while leaving each of those sources alone.

The gap in the WordPress extension model

WordPress gives developers filters, actions and template overrides, but only where the original theme or plugin author has provided them. A filter can change a plugin's text if the right filter exists. A template override can change markup if that package supports one. Once Gutenberg blocks, page-builder output, shortcodes and client-rendered components are combined, the final element on screen may still be difficult to reach cleanly.

Editing a plugin or parent theme directly is the wrong answer because an update replaces the change. Copying a large template into a child theme avoids that overwrite, but also means owning a fork of code for what may have been a one-line amendment. JavaScript can patch the page after load, although that can introduce flicker, miss search engines and accessibility tools, or require custom event code for every small job.

I decided to work with the final output instead. Amender runs after WordPress, the active theme and the installed plugins have assembled the page. It can change a label produced by a plugin, add an attribute to a builder element or remove unwanted markup without modifying whichever package produced it. It is for amending an existing page, not for taking ownership of the whole layout like a page builder.

Representing an edit as data

An Amender change records its target, the action to perform, the aspect of the element to change, the value or linked snippet and the point at which it should run. `replace:text`, `add:class` and `remove:html` are simple examples. The same model covers attributes, inner HTML, wrappers, moving nodes, lazy loading and calling a JavaScript function in response to an event.

The target is shown to the user as a CSS selector because that is understandable in the browser and fits the existing Microthemer workflow. For a server-side change, Amender also stores the resolved XPath needed to find the equivalent node in PHP. Content and larger code values live in named snippets, so several modifications can refer to the same source without duplicating it inside each selector record.

A page builder usually owns and serialises the elements on its canvas. Amender does not. It stores a small instruction against the rendered result, such as replacing the text matched by one selector. Remove that instruction and the original output appears again. The theme or plugin can still be updated because none of its files have been copied or patched.

Running the same idea on the server and in the browser

Some changes should exist in the HTML response. Text that matters to search, an accessibility attribute or removal of expensive markup should not wait for JavaScript. For the `serverHTMLReady` juncture, the PHP layer captures the completed document, parses it with `DOMDocument` and `DOMXPath`, selects the stored nodes and applies the requested action before the response is sent.

Other changes genuinely belong in the browser. A click, hover or in-view animation needs a DOM event and may need to be reversed when that event ends. The client event manager resolves the CSS selector, observes the configured juncture and sends the same action and aspect to a JavaScript implementation. A change can therefore move between server and client execution without becoming a different type of project record.

The two execution paths are not identical. Server rendering uses the visitor's device class to leave out desktop-only or mobile-only changes, and JavaScript functions cannot run while PHP is processing the HTML. Third-party markup can also be valid enough for a browser but awkward for PHP's parser. The editor therefore lets the user choose where a change runs and shows the server result in the review view.

Live editing without losing the original page

The editor runs alongside a live preview of the WordPress site. Selecting an element gives Amender both a human-facing selector and the DOM context needed for the change. Updates to text, attributes, wrappers and positions are applied immediately in that preview, while the persisted record remains the selector-based instruction rather than a rewritten copy of the page.

Each action knows how to construct its inverse. Replacing attributes first records the complete original attribute string. Removing or moving a node can retain a live node reference and its position. Insertions can be removed again as a group. Event-driven modifications keep their undo operations with the active configuration, so toggling or updating a rule can unwind the previous effect before applying the new one.

In editing mode, a `WeakMap` associates each affected DOM element with the snippets that changed it. The editor can then show which amendments apply to the selected element without adding private data attributes to the page. Ordinary visitors never create this map, so the bookkeeping stays in the editor.

Undo and redo across the whole editor

Reversing the current DOM operation handles immediate undo, but users also need to return to an earlier saved version of the project. A saved revision contains the settings and snippets needed to rebuild the result, while revisions fetched from WordPress can also include the related preferences. After the server saves a revision, a Web Worker copies it into IndexedDB and updates a compact stack of revision IDs.

The worker owns the undo and redo stacks so the main interface does not pass large settings objects back and forth for each button state. On a new browser it can fetch recent revisions from WordPress to seed the local store. Restoring an older point moves the current ID between stacks and returns the full revision only when it is needed. Saving after an undo deletes the abandoned redo branch, matching the behaviour people expect from a document editor.

Restoring a revision updates more than the visible text. The selectors, linked snippets, code editors, active dependencies and live preview all come from the same saved state. I found this more reliable than giving each panel its own undo history, especially when one change contains both visual settings and hand-written HTML or JavaScript.

Keeping published pages small

The editor keeps review metadata, undo references and selection information that a visitor's browser does not need. When the user publishes, Amender extracts the active modifications and snippets, separates server work from browser work and groups the browser changes by event, media query and selector.

Server-only edits add no client amendment code. Client edits receive the event manager and only the snippets and dependency map used on that page. A single JavaScript function can be registered against a click, mouseenter, page-ready or custom in-view juncture, which removes repeated `addEventListener` and intersection-observer boilerplate from user code.

Lazy loading uses the same amendment model. The server can remove selected content from the first response, store the fragment under an internal ID and add the small loader that fetches it later. The user can therefore target content visually for a performance change in the same way they would target it for a text or attribute change.

Bringing modern frontend packages into WordPress

Amender includes a package manager because a small amendment sometimes needs a library the theme does not have. Code snippets are scanned for known dependencies such as GSAP, Three.js and Alpine.js. The editor shows the detected package and its weight, then records the user's choice before anything is installed or loaded.

Published modules use import maps and page-level dependency collection. A package can be served from a local installation or a configured CDN, and add-ons are ordered after their core package. Function snippets receive only the imports they use, so adding one animation to one page does not turn the dependency into a site-wide asset.

Tailwind is handled at the same page level. An administrator can add utility classes to any rendered element and compile the required rules without introducing a Node build process to the WordPress site. The compiler works from the classes Amender actually finds, so it adds only the CSS needed by those amendments.

Reviewing what will be published

Draft and published states keep experiments away from the visitor's page. A user can make and undo draft changes, then publish once the result has been checked. The preview can show the amended page, the original page with amendments disabled, or a review view listing the changes that affect the current URL.

The review output includes the selector, action, aspect, event and content for each modification, along with asset sizes and server timing. A user can inspect the same information the runtime will act on rather than relying only on the visual result. If a change is removed, the original theme or plugin output returns because there is no patched source file to repair.

I also made it possible to serve published changes with a smaller loader after the full editor has been uninstalled. On a client site, the authoring interface does not have to remain active indefinitely; the amendment data and the runtime are enough to keep the published result working.

Where AI fits

Amender can be used entirely through point-and-click controls and code editors. The AI assistant writes the same structured changes rather than generating a separate kind of page. It can propose selector rules and snippets, but the existing preview, review, revision and publishing systems still decide how those changes enter the project.

Generating a string of HTML was the easy part of the integration. The editor still had to target the right element, collect enough context, show exactly what would run and provide a reliable way back. Because manual and generated changes use the same amendment format, model output does not get a privileged route around those controls.

What I took from the project

WordPress is a difficult environment for this kind of editor because I do not control the themes, plugins or markup it encounters. The same operation may also need to run in PHP on the server or in JavaScript after an event. I kept each stored amendment small and made its target and timing explicit so the final result could still be inspected when the surrounding site was unpredictable.

The boundary has proved useful: WordPress remains responsible for constructing the site, and Amender owns a reversible layer over the result. Site owners can reach awkward output without turning each small customisation into a fork of somebody else's plugin or theme.