Author: Robert

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

  • 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 Launch List

    I continue to chew through a lot of time making sure that PeakZebra, as a service, works. That it does what it’s supposed to do.

    But I’ve come to realize that one of my major challenges as a founder is training myself not to keep moving the “finished and ready for launch” goalposts. In particular, I have had a tendency to stop at key points, re-envision what it is I’m building, and wind up with a lot more pre-launch work.

    Or more simply: whenever I get close to the start line, I find an excuse to push it further away. A mindset problem.

    Stopping starting

    Thing is, now pretty much all of the necessary code pieces exist and it’s time to just get going with the “having customers” part of the arrangement.

    So I’m drawing up a list of every single thing that absolutely must be done prior to launch. I expect this will be subject to a change or two, but I think I’m close enough that I have a far clearer view than I would have been capable of having early on and can make a list that will remain more or less accurate until launch.

    I don’t think there’s any sense in sharing the list (and, as I write this, the list doesn’t exactly exist yet–I guess you could say I have a concept of some lists), but I hope to make it in a way that makes reasonable time corrections possible.

    By the way, if you’re thinking: “Hey, isn’t this what project management is? How have you managed a big project without some version of this list all along?” Well, fair question, but there have been lists. The problem was that I kept revising the product vision out from under them.

    Plowing mode

    Also: you’d be surprised how easy it is to keep plowing along, once you’re in plowing mode, without considering whether the thing you’re working on actually needs to be done before launch (or at all). “Plowing mode” is critical but also dangerous.

    One additional, related thing I’ve committed to is that I won’t re-write the code base out from under the product once it launches. Revisions of all sorts will of course happen, but no significant do-overs allowed, at least not until the business is up on its feet and humming.

    One open question is the one I mentioned a few days ago: to what degree is what I’m doing going to be like a very specialized agency? This is one that I think the market has to decide, but I need to make the case in a convincing way so that it’s a fair test. And so I guess that had better be on the list as well.

    Lots of things on the list, but not too many to prevent imagining we’ll be running full speed at the top of the new year.

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