Category: WordPress

  • Structuring Code for Cursor and Other AI Tools

    I think this is a post that’s going to evolve over time. That’s partly because I don’t yet–as I’m starting this–understand what I’m talking about. But the thinking here is that while most of the talk of the tubes has been about whether AI can code the way we do, I think the biggest present-day potential win is to find a way to code (and organize code) in a way that makes it easy for AI to digest and do the right thing.

    Part of it is establishing the right context. The Cursor IDE gives you more control over context of any aided-coding tool I’ve seen and I still have a lot of experimentation to do to see how far that can be pushed.

    Give AI a Chance

    But I also think it’s important to think about making requests for code in a way that an AI is likely to get right. I suspect that the basic rule there is going to be keeping things enclosed in very simple functions.

    The other thing I’m thinking about is how best to create tests for these functions. Of course, I’d like the AI to write the tests at the same time that it writes the functions themselves.

    So I asked Cursor to create a function for me. I wasn’t thinking too clearly when I asked, because I wound up asking for something that there’s (duh) already a native core WordPress function for. That said, I convinced myself I’d actually been really clever about because now part of the test was whether the AI would recognize that the native code just needed to be wrapped in the function.

    The prompt:

    write a function for a WordPress plugin that returns true if the current user can manage users and false if not.

    As an aside, I learned that when you use control-K to bring up the prompting box, you can use arrow keys to move up and down through your prompt history (which it gives you a hint for, so I’m not exactly telling you something you couldn’t have found out on your own). I’m pretty sure this will prove useful in life.

    Anyway, the code:

    function pz_can_manage_users() {
        return current_user_can('manage_options');
    }

    Honestly, I think it’s pretty impressive that AI picked up that my request described an already-existent function. We shouldn’t overlook, however, that it is testing the wrong capability. The manage_options capability shows whether you are allowed to make changes to settings in the Settings submenus.

    So that’s a problem. So then, for kicks, I asked:

    now write a function that tests whether pz_can_manage_users works properly.

    And the resulting code was:

    function test_pz_can_manage_users() {
        // Test with admin user
        $admin_id = get_user_by('role', 'administrator')->ID;
        wp_set_current_user($admin_id);
        if (!pz_can_manage_users()) {
            error_log('Failed: Admin user should be able to manage users');
            return false;
        }
    
        // Test with subscriber user
        $subscriber = wp_create_user('test_subscriber', 'password', 'test@example.com');
        if (is_wp_error($subscriber)) {
            error_log('Failed: Could not create test subscriber');
            return false;
        }
        wp_set_current_user($subscriber);
        if (pz_can_manage_users()) {
            error_log('Failed: Subscriber should not be able to manage users');
            wp_delete_user($subscriber);
            return false;
        }
    
        // Cleanup
        wp_delete_user($subscriber);
        return true;
    }
    
    

    Given that the original function is testing for the wrong capability, but a capability that admins have and subscribers don’t, the test function will pass pz_can_manage_users() with flying colors, even though it’s totally not correct.

    I prompted Cursor to fix the first function and it did so correctly. I note also that it did correctly adopt the ‘pz’ prefix I use for functions that aren’t cordoned off within class definitions.

    I prompted:

    in the test function, please add a check for the case where a subscriber has been granted the capabilities to add and delete users.

    And this was added to the test function:

      // Test subscriber with elevated permissions
        $subscriber_user = new WP_User($subscriber);
        $subscriber_user->add_cap('create_users');
        if (!pz_can_manage_users()) {
            error_log('Failed: Subscriber with create_users capability should be able to manage users');
            wp_delete_user($subscriber);
            return false;
        }

    No doubt you’ve noticed that this isn’t explicitly checking for all the capabilities involved in managing users, but I think it’s probably OK to use create_users as a proxy for the full set.

    So, mixed results, but lots of typing saved and, since I’m always forgetting my core system calls, it’s nice that Cursor is capable of figuring out when one can be used and suggesting it with the right syntax (if, in this case, not the right capability to test).

    Constrained requests

    Overall, I think where I want to land with PeakZebra’s code is having pretty much every AI-addressable change request handled by way of creating a callback function and having the right amount of context that the AI can understand what the callback function needs to do (and can do) given the way the codebase is designed. Experiments to come…


  • A Real-World Example of Avoiding Unneeded Prelaunch Dev

    You hear a lot of blather about what the “minimal” in Minimal Viable Product means. And YCombinator has made an industry out of reminding people to launch before they think they’re ready. The trick of course is figuring out where this does and doesn’t apply without kicking the fledgling from the nest while the poor chick is still featherless.

    So I’m preparing to launch PeakZebra in a matter of a handful of weeks, have a list of things that have to get done before that launch can happen, and realized just a couple days ago that the list contained a whole subsection (requiring substantial work) that I can cut without impacting the product release at all.

    Extra work for fun and profit

    I want to share some of the specifics of this discovery because, for all the MVP blather, there aren’t that many useful examples of actually making decisions around just how much P is needed in an MVP. This is also a case where what I’m eliminating doesn’t really have much to do with the product (at least as it’s released).

    Like any SaaS, I need a self-service signup process for new customers. In the particular case of PeakZebra, the way I want to handle that is by having a WordPress site dedicated to the process of signups, billing, and account management. The product itself resides in multiple instances of WordPress with the necessary functionality preloaded into them.

    I spent perhaps half a day thinking about what I’d need on the signup site, debating using an existing third-party plugin (a SaaS subscriber is pretty much the same thing as a “member”, so plugins that handle paid memberships come to mind). I spent a couple hours wondering what should be on the front page of this site.

    Admin can wait

    But then, enlightenment. I don’t need any of this to launch. For early access, I’ll have a form on PeakZebra.com (and yes, this will be a PeakZebra form) and I’ll enter the necessary data in Stripe by hand.

    I’ll need the signup site later, particularly as clients start wanting to manage their account information. I’m kicking a can, but it’s clear to me that none of the work on this should happen before the service itself is feature complete and out in the world with a few customers.

    If this were going to be a high-volume, consumer-facing business, I couldn’t have made this particular choice.

    And by way of counterexample, I’m not foregoing simple onboarding help out past launch. I think “ease of entry” is absolutely crucial to the PeakZebra experience. I’ll keep it to the bare minimum, but the bare minimum is somewhere above the zero mark.

    For every decision to tackle something prelaunch, I need religiously to ask myself: “Why do I need to do this before I have customers?”

  • 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.)

  • What WordPress Should Be

    As I was writing this, Joost de Valk posted a blog piece that points out three key areas of weakness that confront WordPress and rightly points out that we had better face these concerns head on and clear eyed. We need as a community to have the discussions that lead WordPress by developing a vision for it.

    To me, it feels like the current Matt vs. WPEngine drama was a tipping point, even if the problems deValk highlights existed well before. That’s because all of the businesses that primarily live within the WordPress ecosystem were given (forced into) an occasion to think and analyze their own viability moving forward.

    Hammer time

    It was hammered home in a way it hadn’t been in a long time that, with a GPL license, anyone can reuse all your code for anything they like. It was hammered home that the WordPress.org repo is a potential chokepoint run by one guy. It was hammered home that Matt can be unpredictable and he is in a position to hurt your business badly if he cares to. Everyone had a rethink.

    I count myself among that “rethinking” number even though I’m not yet selling PeakZebra. I was in the process of finishing up a large, complex plugin that I was going to sell, most likely along the “free plus premium” model. Though I think Matt caused a lot of harm he didn’t need to cause by going after WPEngine the way he did, it did at least have the fortunate knock-on side effect of making me rethink PeakZebra’s strategy.

    I’ll talk more about that in future pieces, but here I’d like to focus on one of the three main problems de Valk focuses on: the weird and somewhat backward frameworks and overlays of the stack and codebase. As de Valk puts it:

    WordPress is often ridiculed by developers in the wider web ecosystem. It’s considered backward, slow and bloated. And the truth is harsh but simple: this is true.

    I’m a good person to talk about this, I think, because I only returned to coding four years ago, after a long hiatus. I was a programmer for the first ten years of my career but then got pulled away by more managerial and editorial (though still technical) pursuits. I still had programmer instincts but I was way behind on more or less everything.

    Triumphant return?

    The point is, I came to WordPress without too many preconceptions (I’d used it for projects a few times in the past, but more as a power user than a developer and prior to the arrival of the block editor).

    I’ll admit, I wandered back into WordPress because I thought the opportunity might be converting WordPress sites to static Gatsby sites. So I was just as focused on learning React and Gatsby for a while.

    I’ll spare you the details of my earlier programming history, but suffice it to say that React was new territory, as was custom block type development in WordPress with its React components, as was all the action and filter stuff going on it PHP in WordPress.

    I managed all this, but largely because I’d been a full-time, very solid programmer in my past life. And I had done a short stint teaching JavaScript back when it was trivially easy compared to the world of JavaScript today. I had pro chops, even if I was diving into the deep end in a lot of ways.

    Clarity

    You can look at WordPress and see how it arrived at Gutenberg blocks and an editor that runs separately from the rest of the system if you think about the project’s history and motivations. But if you don’t bother to make it a story with a seemingly sensible through-line, if you just look at it from the outside with no preconceptions, it looks like way too many moving parts.

    Again, there are reasons why there are so many moving parts, but it’s not really a system you’d come up with if you were designing from scratch today.

    — added 2/5/2025

    As an aside, though, the bones of modern WordPress aren’t that bad, really, are they? The idea of having a flow that provides hooks where you can tie in your own callback functions is a solid solution to making the core easily extensible without breaking everyone else’s stuff.

    Sure, it’s not the terminology that bleeding-edge developers would use to build a similar system, nor would they, in all likelihood, use PHP, but this is almost more about style than substance.

    And until quite recently, there would have been–were we writing a fresh, new WordPress–a lot of pressure to write something where computation was pushed out to the edge. At a minimum, there would be more active components running in the client browser. But pushing server functions to the edge would have been the “obvious” thing to do.

    Funnily, it feels like pushing server function out of the server ran into precisely the problem you’d expect as soon as you tried to do anything that performed computation using a database. Conceptually, you really can’t divide a database. It has to look, to the higher layers of code, like a unified entity. So for most sites, the server code may as well be running–gasp–on the server. The enthusiasm feels more tempered of late.

    —-

    At any rate, I think part of the work the WordPress community has before it now is asking itself what WordPress might actually look like if it were designed from scratch. I’m not saying that WordPress should be rewritten from the ground up. Too much real-world, useful capability would be tossed in the process. But imagining WordPress without getting too hung up in current difficulties is part of the process, I think.

    Clear eyes might also help us think about the distinctly different directions and needs of enterprise, ecommerce businesses, and blogs. Maybe core is a stripped-down engine with components that extend core for different use cases and levels of complexity. This could be built from the current WordPress, but not without a reimagining of the core architecture.

    In any case, a real discussion, as de Valt suggests, is the way to start.

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

  • Another Side of WordPress SaaS

    One thing about the strategy where you hedge your WordPress bets by offering your wares as a SaaS built on WordPress is that it opens up the question of what’s in your SaaS.

    Once you’ve created a setup where your SaaS customers are interacting with your WordPress server, a potential next step is to incorporate some third-party package into your build and offer access to that as part of your service.

    Anticipated?

    Some of the more complex plugins out there absolutely anticipate this sort of use and have options that let you license the plugin accordingly. Others, less so, and it’s also worth considering that nothing says you can’t take free plugins and offer them wrapped up in your service in the same way.

    It’s easy to see how you might end up with a scenario where you’re wrapping up a paid plugin with a lifetime, unlimited license that doesn’t cost very much and then reselling it heavily. It’s the kind of thing that makes you wonder whether you’re taking advantage, but then again, the unlimited licensing is largely targeting agencies who are reselling the product at a markup, just not necessarily in SaaS packaging.

    By and large, I think it’s legit (though I’m open to counterarguments). If there’s an unlimited license on offer, then SaaS resale seems entirely above-board, from a licensing perspective. And, for better or worse, there’s the fact that GPL licensing pretty clearly requires that you let people take your code and resell it if they want, just as long as they credit you and retain the same licensing.

    The admin side

    One potential problem with a SaaS setup is that your end users may well need at least some access to WordPress admin functions. But you don’t really want them having to learn their way around the back end, probably don’t want them poking around in there in any case, but on the other hand you probably don’t want to have to write a front-end version of the admin functions.

    For this, though, there are a couple of plugins that expose some or all of the back-end interface to front-end users.

    The one that I’m using for this particular need (for the time being, at least) is WPFrontend Admin. It enables you to create front-end pages like this one, where the guts of the user admin interface are nicely nestled inside a front-end page:

    screen shot of a WordPress user admin table.

    It’s a clunky design, just like it is on the back end, but you can pretty it up a bit by overriding various CSS settings. I haven’t really dug into that yet, but what this gives PeakZebra is a way to allow clients with the right access privileges to manage the user’s they’re allowing on their instance of the PeakZebra service.

    PeakZebra users

    As it happens, I’ve also written a spinoff of the PeakZebra form blocks that specifically map to WordPress user account fields (and meta) and that are specifically for use on the front end. They’re implemented using the Interactivity API which means they will at some point confer special, magical powers.

    But implementing a full copy of the user admin interface at this point in PeakZebra’s development seems like a pretty significant detour, given that I can just surface the user admin functionality on the front end using the plugin.

    The thing about the Frontend Admin plugin, though, is that it’s expressly built for the scenario where you resell a complex plugin. Let’s say you take the Groundhogg CRM. It basically lives on the admin side of the site where you install it. But you can make all those pages magically available on the front end with the Frontend Admin.

    And this trick works with all sorts of plugins. Check out their list of common applications used this way.

    I don’t think this is the future of WordPress, exactly. But we’re going to see more cases where people are using this sort of setup to keep their own modifications hidden on the server side of things. Whether this is a good thing is debatable, but I think overall it’s a good thing to have a business ecology around the core open-source WordPress project, and the status quo of how business exists in the WordPress world is clearly being seriously re-evaluated right now.

  • Common Codebase Versus Custom Deployments

    These days I’m spending a lot of my time making sure the PeakZebra product works.

    But it’s clear to me that quite possibly the biggest challenge I have before me is dealing with the dead certainty that there will be lots of customizations made to client instance of PeakZebra. In a way, that’s PeakZebra’s secret weapon: you get exactly what you need for your app.

    I know how to manage this is a general, not all that optimized way, but I’ve started thinking about how to scale this so that I can handle thousands of clients with lots of different deployments.

    Deployment?

    To start with, even what counts as a deployment needs tighter definition. There’s a lot of code in PeakZebra, and that code is tracked using git and GitHub. There needs to be a good way to deal with having different versions of the actual code across different deployments, because that’s definitely going to happen. It’s going to happen lots, I presume. So it isn’t as simple as having

    Second, a production WordPress site is a big amalgamation of things that profoundly impact the site’s look and behavior, but that aren’t necessarily code that would be checked into a repo somewhere. You can add custom post types using a plugin, for example, and it’s genuinely important, should you need to recreate the site, that those custom types be checked in.

    Also, many changes that are made to a WordPress-based app result not in changes to code, but to changes in the database. We don’t really want to check in a new copy of the database every time anything at all gets tweaked, do we?

    Tracking the combinatorial

    But what’s the best way to track all this amalgamated stuff?

    One could, in theory, just keep a full backup of each site (which is, not surprisingly, what most developers and agencies do). That doesn’t really answer the needs of the “backside” of all these deployments.

    Whatever the system is, it needs to be quick and efficient for a developer to make changes to each system. Yes, there’s a more or less common codebase. But in order to make changes, the developer needs to know what other customizations have been made to this deployment and how to avoid inadvertently breaking prior modifications.

    And there’s another twist. Some of the changes will make sense to propagate back to the “original” code base. But how will the developer know they aren’t going to break other sites with other modifications when the next update to the original code rolls out to all the other deployments.

    Non-computable?

    There’s almost certainly a “computer science” way to prove this is actually an unsolvable problem. But before we panic, let’s note that most changes, most of the time, won’t matter to other sites and aren’t likely to break much of anything else. Not in the WordPress world, where things are always running with the assumption that each site is cobbled together with a combination of parts in addition to the core code.

    Basically all I’m aiming to do in this particular blog entry is lay out the problem set, but let me say a few things about how I’m viewing a solution.

    First, I think there probably needs to be a scrubbed-data version of each deployment stored on local servers in a “ready to use” status. Imagine hundreds of local deployments, each with a copy of the code used in the relevant PeakZebra plugins.

    Given that WordPress stores revision histories for posts, this capability with regular backups, plus git repo storage for all the actual code, could potentially solve the problem of being able to track thousands of revisions across thousands of sites.

    What it doesn’t do is help the developer not shoot themselves in the foot while updating a particular site. More about this in future posts.