Category: WP DevOps

  • Creating a Block with Cursor AI

    I’ve been trying to figure out what the best approach to getting the most productivity out of AI-assisted coding in Cursor. Some things work jaw-droppingly well. Others create a rabbit hole of inexplicable coding failures that are more trouble than they are worth to debug and make work.

    Here’s a very simple example of something I was working on this morning: I wanted a WordPress block that would show an alert on a page with whatever message I put into it, and I wanted it to disappear on its own after it had been on screen for ten seconds.

    Keep it simple

    The takeaway, if you don’t care about the details, is this: you should stick to relatively discrete steps you’re asking it to achieve and you should check each bit carefully as you assimilate it into the code.

    As a placeholder, I’d been using a block from the WordPress repository called simple-alert-blocks. The simple part of the name doesn’t mislead. Nothing wrong with that, but it doesn’t fade.

    So I thought, I’ll add Cursor to make changes to it so that it always disappears after ten seconds.

    Only too happy to oblige

    Cursor happily did all this–it even looked pretty good as code, just scanning over it–and it didn’t work. Absolutely nothing happened. I started by asking it to debug the problem and it happily provided me with a “fix,” except that the fix simply re-applied code that was already there. So this improved nothing.

    I took a few minutes to look at it, but wasn’t seeing the problem. Later I realized that it had probably created a mismatch between the class name it was applying to the message box and the class name it was using in the css file. The class in the css file was .wp-block-pz-alert whereas the actual element in the DOM was using .wp-block-create-block-pzalert.

    I say this was most likely the problem because I didn’t have the code by the time I figured it out, but that CSS mismatch was a theme throughout the whole process.

    Keep your context ungoofed

    Leading to another takeaway: if Cursor does something substantially screwy, don’t just fix the problem, reset your context so that it isn’t still chewing away with the wrong idea in the back of its mind somewhere.

    So then I asked it to just create a block that did what I wanted from scratch. This one also looked pretty good, but wasn’t registering the block once I’d activated the new plugin (for once I didn’t forget the activation step). Again, strong suspicion that the mismatched CSS was in play, but there was also some idea that I’d inadvertently introduced because the original simple alert box was still in context that there should be a separate .JS file that had a “fade” function in it rather than just including this in the view.js file.

    Doublechecking

    At this point, I decided to take the AI part in far smaller steps and now created a new block using the create-block script. From there, I asked for specific steps and checked that each step worked. It occurs to me that Cursor currently makes about the same number of mistakes as I do, so I should doublecheck my progress in more or less the same way.

    This doublechecking process eats away at the time advantage of having Cursor just blast out a whole plugin, but is still way faster than my usual process. This is in part because it remembers all the function definition and syntax details that I routinely forget. It’s massively more capable than the sort of autocomplete you get with something like Github’s Copilot.

    Even in this process, it fouled up the CSS again. But it did lots of other things–using details that are very specific to WordPress block development–and had no difficulty with it.

    I asked it to add an attribute to the block, one called “messageText”:

    Note, though, that it decided to freelance on the “name” attribute. I did not tell it to create a pz domain and there were lots of other blocks in context that do it the way I wanted it, so conceivably it might have figured it out.

    Anyway, adding the attribute worked just fine, but in typical computer fashion, there were plenty of obvious related tasks that I had to specifically ask for.

    Initially, the suggested changes didn’t include importing the TextControl component, so it didn’t work and this is one of those problems that doesn’t really throw any errors, it just quietly refuses to register the new block.

    I asked it to fix this and it replied with the cheery bullshit one sometimes gets from LLMs:

    And then the line was there. But you have to wonder…

    Anyway, when I really got down to brass tacks and was explicitly asking it to make changes in chunks that I could quickly doublecheck, the process went quickly. It would have gone even quicker if I’d reset the chat and started with fresh context.

    More reports from the coding trenches to come…

  • The Current Site Versioning Plan

    I was thinking about my current plan for keeping track of lots of slightly variant iterations of a baseline WordPress site blueprint, and it hit me. Duh. Disk space doesn’t cost much. You can rack up dozens of terabytes of storage without maxing out your credit card.

    Indeed, this is almost the definition of what a hosting service does. Thousands (tens of thousands) of sites saved on a lot of cheap disk space.

    So yeah, just make a full copy of everything. (Actually, there are incremental backups for WordPress these days and I wonder how effective they are or aren’t. Must explore.)

    A million full images!

    So every time I change one of the changed sites, I just make another copy of it on disk?

    Well, no. Not that simple. For one thing, if you make hundreds of individual changes to thousands of sites, even your cheap terabytes are going to whimper and demand higher pay. Copying all that stuff around is going to give you performance fits, too. As per the above, though, if incremental backups shave off enough of the bulk of each backup, maybe this works.

    Still, I want to be able to answer questions like: what’s the difference between customer X’s current version of the blueprint and customer Y’s? So I need some kind of artefact that I can run a diff against.

    Starting positions

    So imagine we start with a blueprint site. What’s in it or how it got to that state doesn’t matter, but it’s a known starting point and we won’t do anything to any site using this blueprint without tracking the changes.

    There are perhaps five categories of things we might do to a given iteration of a blueprint site:

    1. Add or delete a third-party plugin
    2. Add, delete, activate or deactivate a third-party theme
    3. Change configuration settings
    4. Change code for plugins or themes we’ve created
    5. Make changes to posts (of any type), create new posts, delete existing posts

    However it is that we wind up tracking these things, we’d also like to manage questions around whether the changes we’re making on a given day will remain unique to this client or should be incorporated back into a future version of the blueprint.

    Let’s start, though, with how to do the tracking in the first place.

    The first three items on our list are things that WordPress folks often do manually. But they can also be done programmatically from, say, a plugin. And they can also be carried out by WP-CLI (and you can write your own WP-CLI commands, so there’s a lot of potential power here). So you can imagine batching commands into a bash file and version-controlling that file.

    Keeping track

    The problem with this is that the way developers are used to working with version control is that they make a bunch of changes to some code and then commit it to the repo. You can’t manually add a plugin and then commit that to the repo. You’d have to figure out what you were trying to do, add the needed ingredients, establish that it worked, and then edit a command file so that it made those changes.

    Then you’d have to restore your test site back to where you initially started, run your automated update, and then test again to make sure you didn’t leave anything out of the instructions.

    This sounds like a wonderful discipline for making sure you really have accounted for every change you’ve made, but this is going to be twice the time and work. Not ideal.

    Since I control the environment overall (the blueprints and how they are used), one tactic for plugins and themes is simply to have a list of all of them that might be in use. An easily-produced tool can then simply take a look at the site and make a plugins.json file (or something like that) that has all the installed plugins, whether they are activated or not, and potentially all of their configuration information that’s not stored in standard WordPress database tables (we thought we might be able to leave that part to VersionPress, about which more in two secs, but no)

    Changes to our own code we know how to track using git and related tools, though we’ll also need to track the resulting distributable versions (zip files) of these plugins. (There may, actually, be a staging environment where the full development environment is installed for each client. In fact, I’m fairly sure we’ll need to do that, as I sit here thinking about it.)

    Alas, VersionPress

    Changes to the database (where the layout in posts, current changes to FSE themes, and so on) I had hoped I could at least attempt to track with VersionPress. My limited understanding of VersionPress was that it would work for standard WordPress database changes and plugins that behave nicely within the WordPress universe (by which I primarily mean that they only write to existing WordPress tables).

    If you go back and look at VersionPress information online, you can see that it was basically the magic bullet. There’s even a slight possibility that it still could be. But when I started poking around the Github repo for the project, I learned the sad truth that VersionPress’s creators stopped work on the project back in 2020. It’s still fully available via the repo, but there’s no support other than digging in yourself.

    This is too bad, because it looks like it did precisely what I wanted. A potential saving grace is that it might already do, in unfinished form, what I need. But I don’t want to take a big detour learning my way through the VersionPress codebase, so it either works on install for the changes I’m trying to track, or we’re looking for another approach. (I mean, who’s really got time to fork a complicated project?)

    Since virtually every PeakZebra plugin writes to tables that are outside the standard WordPress tables, changes there wouldn’t be tracked by VersionPress without some (probably fairly major) alterations to the VP code. But I’m pretty sure that’s OK. What’s being written is mostly end-client data (prospect names and addresses, for example) and we’re absolutely not trying to track the customer’s data (and, just in case you’re worried, won’t be using it on development copies–we will, though, be tracking changes to add edge cases and such to a pair of fake customer and other data we’re using for testing).

    I think this could work, but a huge number of changes have occurred in WordPress since work stopped on VersionPress.

    If VersionPress isn’t the answer, tracking database changes becomes a harder task. If variations from the blueprint code that we provide are stored in a separate client.php file that registers callbacks to fire at the appropriate moment, we’ll get lots of mileage from just tracking the dev source for the plugins and then the client.php file for each client. But hopefully we can do the more complete version I’ve outlined here. I’ll report back, and in the meanwhile I need to go install VersionPress and see what it can (still) do.

  • AI Coding and Context

    It’s clear that the way to get code “as if you’d coded it yourself” is to include your coding in the context that your LLM is using. Cursor, which I’ve been experimenting with of late, gives you several layers of potential customized context, and I finally found a few minutes to explore how this works.

    In case you’re wondering, Cursor is a new kid on the block and the documentation is correspondingly minimal. Note, though, that you can simply ask the AI for an explanation…

    A screen clip where the AI is prompted about setting context and replies that it can't "directly help with Cursor's functionality"

    Oops…actually it doesn’t include Cursor itself in its context. I’m not actually knocking this — it makes sense from a context management point of view.

    how can it not know what it is?

    Anyway, the basic idea is that there are some @keyword directives that allow you to include things in the context of the current chat (and, of course, different chats can and will use entirely different contexts.

    So if you type @ in your chat window, you get a list of options that includes @folders, where it suggests the current working directory, but you can put anything you like in there, folder-wise. Interestingly, it will tell you if it doesn’t think you’ve picked useful files for context:

    Looking at the code, I don't see any files that would be relevant to the user meta functionality we were discussing. The files shown are mostly block-related files from the pzdata plugin, containing JSON configurations, CSS styles, and asset dependencies.
    
    To properly help with the user meta issue, we'd need to see:
    
    The file where get_user_help_preference() is defined

    The world inside an AI-powered IDE is a weird place, though. Consider, for example, that it was Cursor’s AI (channeling claude-3.5-sonnet) that wrote get_user_help_preference() in the first place.

    For my own purposes, I want to be able to say, listen, when asked to add this or that capability or configuration to the program in question, please solve it using this algorithm, inside a function named according to this pattern, and invoking this function from this kind of callback.

    Straight out of the box it seems pretty darned good at figuring out that it should follow your naming conventions (though I suspect this relies on your having been disciplined about following them yourself), but what about making it expect to be on the business end of a callback?

    Intriguingly, there’s an @ directive for @docs. As is the case absolutely everywhere in the documentation, the explanation of what it does is pretty sparse: you can point it at the documentation for…whatever you like. They are assuming regular framework or library documentation, things like the docs for NextJS, but presumably you could try adding something like a coding standards document or rules like “make these changes by hooking the ‘init’ event and making this a callback function.”

    I thought there might be a deeper dive into this in Cursor (the company)’s blog, but things are pretty sparse there, as well. That’s not a knock, either–these folks are busy making the donuts. It’s interesting to read their post of a year ago about the problems they hope Cursor will solve. The vast majority of what they’re taking on is, in one way or another, a question of managing context (or to put it another way, managing tokens).

    Just to give this a preliminary once around, I created a file in my current project called instructions.txt:

    If you are asked to create a function called "my_response()", make it a function that writes a short message to console.log.

    No sooner had I saved this than it (because it was in the current working directory, which I’d included in its entirety) was picked up and my edit window was prompting an edit completion for the function header, but I could also use the “generate” option in the editor, where I asked for a function with the magic name:

    a chat box asking for a function called "my response" and offering the complete function as an update.

    It’s a trivial case to be sure, but oh my god does this seem promising.

    (Final note: I don’t think I’ve ever used a meme GIF in my writing before. I don’t know what came over me.)

  • Salute Your Cursor AI Overlords

    I’m not sure yet, but I’m pretty close to sure that AI can’t do anything genuinely complicated when it comes to writing code. Or, more accurately, it’s happy to do immensely complicated code creation, but most of the complex stuff doesn’t work and is more trouble than it’s worth to fix.

    But a recent return to experimentation with ChatGPT impressed me enough that I think, within a narrow context and with requests that mimic other code in the codebase, AI should be able to handle requests like “change the blah field verification routine to add a check of whether blah.count > 5 and if it is, then return fail”.

    My being convinced won’t buy you very much at the programmer store, but I’m nevertheless convinced. What’s more, I think there are huge tracts of PeakZebra that, with some careful refactoring, can be structured so that AI makes reliably accurate changes on request.

    I’m saying ‘AI’ and not ChatGPT or some other LLM model/service because I simply don’t know enough just this moment to say which AI is mostly likely to do what I’m after.

    Cursor buzz

    That said, Cursor is the current candidate. There’s a positive buzz out there on the pipes but, far more important than that, Cursor looks at your codebase as it formulates its answers. My hope is that this will enable it to handle “basically the same thing that we did over here” type requests.

    I’d love it if I could set up some rules for determining which kind of solution is best, but I simply don’t know nearly enough to know what that would look like, if any generally available LLC out there also supports traditional rule-guided expert system elements, and whether the real answer is to build a custom, local machine learning system to do the coding instead (unlikely).

    To take one example of the sort of thing I’d like to be able to do, remembering that I’m doing this in the context of WordPress installations, I’d like to be able to say “add a hook to the blah action, write a callback function for it that follows my function naming conventions, have the function do blah, and then store this function in the client.php file.

    In an ideal world, you’d then ask the AI to write a function that tests the new function and can be added to existing test regimes. No particular reason, with the correct (but perhaps not yet existent) connectivity, it couldn’t then run the test against the procedure to make sure it works as expected.

    To me it sounds a little “out there” when I consider my wishlist as a whole, but then again, aren’t the various pieces of all these functions more or less already out there? Especially for use in a fairly well declared framework like WordPress?

    Cursory beginnings

    So I downloaded Cursor and am giving it a spin to see how much it can do for real world programming before it breaks up on the rocks of code complexity. I’m midway in the free trial period, and things are looking extremely promising with it as a way to let the magic box product a lot of the code-talkin’.

    Downloading Cursor was something I recently threatened to do, so if nothing else I’m leveling up on blog promise follow through. I won’t spend time describing Cursor, because odds are good you’ve already heard something about it. In any case, lots of other great general writeups are already out there.

    WordPress Knowledge?

    My big first question was whether it would be accurate in its understanding (?!) of the WordPress context. So I opened up the plugins directory on a local instance where I’ve been working. I can’t really say how well it has a grasp on the relationship of the several plugins I have in there, but it’s definitely looking at all the code in the directory.

    While context is king in AI, it occurred to me that even on a dev installation, you may well have several third-party plugins installed. Best not to waste context on what could amount to a whole lot of code that has nothing to do with your dev efforts. So, probably better where Cursor is concerned to mash everything into one plugin and open that single plugin’s top folder, rather than the plugins folder itself.

    Better yet, perhaps one can simply move the third-party stuff to the MU (must use) plugins directory. I’ll give that a spin and report back.

    Working in Ignorance

    I’ll confess that I haven’t yet taking the time to really learn what’s going on in the Cursor interface. Even so, I figured out how to open a chat window alongside a file I was working on and have a chat that was aware of the open file(s) I was working on. (This isn’t actually the best way to generate code within existing files and codebases.)

    It seems to work pretty darned well to have a specific thing in mind that can be packaged as a function. So you can prompt things like “write a function that takes the task and the rate, consults a table for the standard number of hours that should be estimated for that task and returns the estimated revenue for the task. It seems generally able to figure out things like “task and rate” will need to be variables. It even seems to put together a lot of things like “rate times hours” is what the answer is. There’s a fair likelihood that it will understand that, absent any further context, it will need to create a dummy table with some sample tasks and hours and it will give you a vaguely pedantic explanation in its response that says all you need to do is replace the dummy values with your real ones.

    For example

    I wanted some help text that appeared on relevant pages and sections in PeakZebra and I wanted each user to decide when the help text boxes could be dismissed and not be shown anymore. Plus I wanted any individual user decide they needed the help again and be able to toggle the help boxes back on.

    I did this in a few discrete steps. First, I asked for a function that would add a flag variable called is_help to each user’s metadata. Then I requested code to add the field to each user’s profile page. Cursor did this with no issues.

    I asked it to create a block for my help text:

    It gave me a two-file setup that looked pretty good but didn’t quite work:

    help-text-block.php

    <?php
    /**
     * Plugin Name: Help Text Block
     * Description: A block that displays text only to users with is_help meta field set to true
     * Version: 1.0.0
     * Author: Your Name
     */
    
    if (!defined('ABSPATH')) {
        exit;
    }
    
    function help_text_block_register() {
        wp_register_script(
            'help-text-block-editor',
            plugins_url('block.js', __FILE__),
            array('wp-blocks', 'wp-element', 'wp-editor')
        );
    
        register_block_type('help-text/conditional-text', array(
            'editor_script' => 'help-text-block-editor',
            'render_callback' => 'help_text_block_render'
        ));
    }
    add_action('init', 'help_text_block_register');
    
    function help_text_block_render($attributes, $content) {
        // Get current user
        $current_user = wp_get_current_user();
        if (!$current_user) {
            return '';
        }
    
        // Check if user has is_help meta set to true
        $is_help = get_user_meta($current_user->ID, 'is_help', true);
        
        // Only show content if is_help is true
        if ($is_help) {
            return $content;
        }
        
        return '';
    }

    and block.js

    (function(blocks, element) {
        var el = element.createElement;
    
        blocks.registerBlockType('help-text/conditional-text', {
            title: 'Help Text',
            icon: 'text',
            category: 'common',
            attributes: {
                content: {
                    type: 'string',
                    source: 'html',
                    selector: 'p',
                }
            },
            
            edit: function(props) {
                return el(
                    wp.blockEditor.RichText,
                    {
                        tagName: 'p',
                        className: props.className,
                        value: props.attributes.content,
                        onChange: function(newContent) {
                            props.setAttributes({ content: newContent });
                        },
                        placeholder: 'Enter help text here...'
                    }
                );
            },
    
            save: function(props) {
                return el(
                    wp.blockEditor.RichText.Content,
                    {
                        tagName: 'p',
                        value: props.attributes.content
                    }
                );
            }
        });
    }(
        window.wp.blocks,
        window.wp.element
    ));

    I’ll use subsequent posts to talk more about what does and doesn’t work with Cursor, but I want to highlight something here and right off the bat: this is arguably the “old school” way of creating a block. These days, you’d expect most of the block to be declared in a separate block.json file. And you’d expect a javascript file that needed to be run through npm start to generate a finished version in a build directory.

    Does it dream of electric sheep?

    And here’s the weird thing about working with something like Cursor (and the various LLM models it uses in turn): it’s possible that this is a savvy choice. After all, it avoids the whole process of processing. It’s ready to go as soon as it turns up in the plugins directory.

    One argument in favor of seeing it as savvy is that it correctly wrote the JavaScript file in vanilla script. That’s why it calls createElement right out of the gate and why it looks weirdly overcomplicated.

    On the other hand, maybe it’s just modeling outdated stuff on the internet. There have been lots of changes in the “typical” block build over the past year and thus there are lots of bits of sample code out there that do things in ways that work but aren’t really what developers are currently doing.

    I was thinking it was possibly picking up older blocks in my code base, because some of them still have earlier techniques in them. But it occurred to me that I’ve never not used webpack in my block building, so presumably not.

    In any case, you can’t tell what it was thinking or where it was getting its thinking from, as far as I’m aware. I think chat context is probably the way to help control where the magic gets its smarts, but I have a lot to learn on that front.

    I’ll discuss actual code quality (as well as having Cursor debug its own code) in the next couple of posts.

  • Client variation management

    A key capability PeakZebra needs to develop moving forward is something that, strangely, doesn’t quite exist in WordPress. The capability: completely manage revision control across entire sites (that is, anything at all that changes on a site) and be able to scale to do it across a large number of sites (several thousand, say).

    At the same time, this process has to supply easily grasped context for a developer who knows the product setup but has never worked on a particular site. That developer needs to be confident, when making a change to the site, that the changes won’t break when future changes are made to core elements of the site and that core changes that need to occur to make changes on this particular site won’t break all the other sites using that core code.

    What makes it hard is that WordPress isn’t just a bunch of flat PHP files, but instead keeps all sorts of things in the database. From inside the database you can, to some extent, track changes by way of WordPress’s built-in revision tracking. From outside you have a lot of tools for the flat files, but no clean way to track the database. When people talk about version control in WordPress, they are usually only talking about the flat files.

    Three options + 1

    The easiest way to make total site version control happen–something that agencies routinely do–is just keep dated backups of full sites. But this works largely because any code they write for the site is in version control and everything else is eventually recoverable by returning to a prior backup.

    The “backup everything” approach falls down on the need to make changes to a lot of sites reliant on one key set of custom code. It’s not a problem at agencies because there are enough design and functional differences among clients that they are all effectively one-offs, even if they share some custom code here and there.

    The “+1” option in the subheader above refers to VersionPress. And I should say that if I understand correctly everything that it covers, VersionPress has executed a full-blown solution to what I need. It’s just that the project leaders keep insisting that it’s not yet ready for production. Be that as it may, I’ll be digging into it because I might be able to find a way to keep it simple, thereby keep it from breaking, and save my self an enormous amount of time trying to find some hacky way to do it myself.

    Just the code

    Another option is to focus only on version control of the custom code written for the sites.

    This works great if everything is somehow reflected in the code, but there are lots of things that don’t magically wind up in code. What if changes to the configurations of third-party plugins are required for proper operation?

    Don’t touch core

    The approach that WordPress uses is useful here: don’t touch core, only let’s extend that to a second “core”, namely the PeakZebra baseline instance (the site blueprint).

    If a change to a site really and truly does have to be made to a site, then either make that change such that it works generically across all instances with whatever changes (a hard thing, frankly, to determine with certainty–lots of testing involved to do it right), or flag it so that the changed code only runs with this particular sort of instance.

    This doesn’t handle configuration changes to other plugins, of course, nor does it handle things like changes to application-relevant pages, changes to icons, and so on.

    Only change what you can make trackable

    The solution PeakZebra is currently trying to keep capable of covering all the bases begins with “don’t touch core”, which seems like absolutely the easiest way to keep an orderly shop.

    But we’ve also introduced a “migrate.php” file that borrows a bit of thinking from Ruby on Rails, where you have instructions to migrate from one database configuration to the next.

    Here, though, migrate.php is designed to execute any changes you need made that aren’t normally made in code. In particular, before we start using a third-party plugin, we figure out how to capture configuration settings (sometimes as simple as updating regular-old WordPress meta) and use the migrate.php file to overwrite configuration settings to get them the way we need them.

    For changes that wind up embedded in posts in one way or another, we use import (run from wp-cli) to bring in a data set (stored in a .WXR file created by exporting from the development system).

    Getting it all running in an automated, coordinated way is, to be honest, something we’re still getting ironed out, but it’s really the concept that I’m trying to get at. I hope to provide some concrete examples in future posts.

  • Slots to Slotfill, Callbacks to Call

    So, one key challenge to tackle is managing changes to individual PeakZebra deployments without creating variants that can’t be merged back together.

    My initial thought was the WordPress slotfill capability, probably because I was aware of it but didn’t know all that much about it. I did a little poking around, enough to realize that it’s only relevant in React contexts. In other words, could well be relevant to situations where blocks are going to be rendered, but otherwise not the droids we’re looking for.

    Single file of truth

    So we’ll get back to slotfill at some later date when other more pressing versions of the broader problem have been worked out.

    So, an initial thought is that, if at all possible, per-client changes to PeakZebra’s base deployment should be stored in separate files, perhaps with there being one agreed-upon file that is always included and executed on every deployment. Think of it as a kind of functions.php file, but for PeakZebra plugins, not themes.

    Given the SaaS nature of PeakZebra, one could even contemplate just putting client-specific stuff in the actual functions.php file, but that seems like a potential long-term creator of problems.

    So the current approach is a file called client.php that will primarily register and hold the code for a bunch of PeakZebra specific callbacks.

    But which callbacks?

    So far, I’ve used the facility that’s build into WordPress core for this, but I suspect I really need the callback functions to have an option to completely replace a function within PeakZebra. This of course creates its own problems (how does the replacement function keep synched with code it’s taken over but that has changed in the original version since it was carved away?

    One potentially nice benefit of routing everything through client.php is that, for the foreseeable future, each client’s version can be stored as a unique branch in a single git archive. That should be acceptably performant (and it’s performance during dev, so it’s not about top performance in any case) up through several thousand clients, at which point the client.php files may need to live in their own database.

    So each client’s series of modifications, at least in actual code (as opposed to configuration and content changes) will be stored throughout its revision history and always retrievable by branch.

    This leaves, for another day, a whole lot of questions about which hooks are needed, and so on. We’ll get there.

  • The WordPress Version Control Divide

    I was checking out a podcast video by Brian Coords when I hit upon an exchange that both outlined the difference between a developer/workflow-based approach and a more traditional WordPress approach to managing changes on websites.

    What I love about this conversation is that both interlocutors are obviously not only smart, but smart about WordPress. I’m familiar with Brian from Bluesky and X, but hadn’t run across Mark Szymanski, a WordPress content creator. Mark enters the conversation hoping to get insight from Brian into the whole version control thing.

    Inside, outside, upside down

    There’s a moment early on where Brian lays out an important distinction between people more accustomed to doing everything inside a WordPress installation’s logged-in environment and those more accustomed to writing code outside of the running system and then deploying it.

    “This is the biggest core concept that people are struggling with with WordPress and the block editor. … If you came from page builders where you do everything … inside of WordPress, you’re not writing code, you’re not in the code base,” Brian says.

    “Versus other people who come from a heavier code background–they want to do all that stuff in code and then put that on the site.”

    One thing that fascinates me watching their discussion is that they spend a while in the early going almost but not quite talking past each other. It’s a clear indication of the kind of difference between “in-frame” designers and “out-of-frame” developers. On the one hand, Coords is mainly talking about code and configuration files that can be checked in to a git repository. Typically this would only apply to the files you’d create and maintain to build a plugin (perhaps one that creates a custom block) or a theme.

    Syzmanski, on the other hand, is talking about tracking all the sorts of changes a person might make by adding a plugin to a site, or developing a page using a page builder like Elementor.

    Database difficulties

    And this latter category of stuff, which in the podcast they agree is “content,” broadly construed, really doesn’t fit in any obvious way into the world of Github repositories and code deployment.

    New to me was an approach used by high-end enterprise WordPress sites: “code goes up; content comes down.” Roughly speaking, this means you can’t write things that will be stored in the database “up” from a developer’s local copy of the site to the production site. That “content” can be copied down, but not up.

    What’s allowed to go “up” is code, by way of being committed to a repository and then deployed to production from there.

    No self control

    Alas, it became clear that they weren’t privy to some deep magic way to version control the “content” part. This doesn’t exactly surprise me–I think it’s pretty much where the vast majority of us have landed–but it became clear that the discussion was mostly about how people approach managing specific directories of code in WordPress. If you’re developing a custom plugin, for instance, you’d have directories for source PHP and JavaScript files (as well as things like .JSON files) that you can send to a repo just like any other programming project.

    Meanwhile, though, I need a way to track this stuff and my own take on this is that what’s needed are version-controlled “make” files. Each file is a set of code instructions to move from one version to the next (across the whole WordPress site).

    I’m glossing over a lot of detail, but my thought is that the way to keep things honest is to have a staging site that you can only update by submitting a make file (or a batch file). Possibly this can all be done via WP-CLI. I’ll have to do a little poking around.