Category: WordPress

  • More Cursor Programming in WordPress

    I just spent about thirty minutes creating a couple of functions. I can’t really say I wrote them; it was almost entirely done by AI. I think it’s a nice example of ways in which Cursor and AI can really shine, so I thought I’d spend a couple minutes walking through how it went.

    PeakZebra (the system, not the company) has a SQL table that stores data about each of the other SQL tables that PeakZebra creates and uses. The format (for better or worse, and I suspect it’s probably worse) is that the string you’d hand off to the dbDelta function is stored in its entirety.

    The table of tables table

    While initially building PeakZebra, this made it easy for me to quickly change the fields in any of the (now rather numerous) tables I use. I’d just jump into a PHP file where each of these strings was added to the pz_table_str table. An example looks like this;

    // interaction table
    $table_name = $wpdb->prefix . "pz_interaction";
    
    // interaction table
    $item = array();
    $item['id'] = null;
    $item['table_name'] = 'pz_interaction';
    $item['field_string'] = "CREATE TABLE $table_name (
      id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
      per_id bigint(20) NOT NULL DEFAULT 1,
      summary varchar(255) NOT NULL DEFAULT '',
      details varchar(800) NOT NULL DEFAULT '',
      created varchar(12) NOT NULL DEFAULT '',
      PRIMARY KEY  (id)
    ) $charset;";
    
    handle_def_record($item);
    dbDelta($item['field_string']);

    the handle_def_record($item) call writes the string to the definition table. Then dbDelta actually creates the file.

    If you’re not familiar with dbDelta, it’s pretty handy. It creates the file you’ve described if it doesn’t exist. But if it does exist, it examines it and adds or deletes fields as needed based on comparing the existing file to your CREATE TABLE string.

    I wanted to add a way for users to add fields to tables (and perhaps to create new tables), so I wanted to grab an existing string from the table with the defining strings in it, parse it into the individual fields, and then present a form where new fields could be added (deciding to worry about deleting fields later).

    Toward that end, I opened the PHP file of a “table” block I’d recently created and gave Cursor this prompt:

    let's add a function called read_table_def that reads the record with the table_name field equal to 'pz_person' and returns the field_string value for that row. 

    Cursor gave me this:

    // ... existing code ...
    
    function read_table_def() {
        global $wpdb;
        $table_name = 'pz_person';
        
        $result = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT field_string FROM {$wpdb->prefix}pz_table_def WHERE table_name = %s",
                $table_name
            )
        );
        
        return $result;
    }

    Some things of interest here:

    1. Although it could confer that I was working within WordPress from the file I’d opened, I didn’t explicitly say so.
    2. It figured out and knew enough to know that it would need the $wpdb global.
    3. It wisely uses $wpdb->prepare(), something I’ll admit I’m often to lazy to bother with.

    Now, the code it supplied was correct, but I’d screwed up and told it the wrong file name, insofar as I’d forgotten the WordPress database prefix.

    Debugging and debugging the debugging

    As a result, I asked it to add a bunch of debugging code, which it did, leading me to realize, within ten minutes or so, that I’d screwed up.

    At that point, I asked it to add the prefix, and it did:

    function read_table_def($table_name) {
        global $wpdb;
        
        // Debug: Print the actual query
        $query = $wpdb->prepare(
            "SELECT field_string FROM wp_pz_table_def WHERE table_name = %s",
            $table_name
        );
        echo "Query: " . $query . "<br>";
        
        // Debug: Print the table contents
        $all_records = $wpdb->get_results("SELECT * FROM wp_pz_table_def");
        echo "All records: <pre>";
        var_dump($all_records);
        echo "</pre><br>";
        
        $result = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT field_string FROM wp_pz_table_def WHERE table_name = %s",
                $table_name
            )
        );
        
        // Debug: Print the result
        echo "Result: ";
        var_dump($result);
        echo "<br>";
        
        return $result;
    }

    Of course, I didn’t want the prefix hardcoded. An easy fix. I asked Cursor to remove the debug code, which it did. Then I moved on to a function that would take the string and parse it into an array of individual field definitions.

    I’ll skip over the details, but once again, I’m fascinated by the things it “figures out” on its own, for example, telling me that it’s making the assumption that the fields are comma delimited, but it’s an easy fix if it’s not right about that.

    It’s strangely like working with an actual mental process. It’s probably terrible news for junior programmers, if for no other reason that it pretty much never screws up things like the parameters and formats of arcane system function calls.

  • The Creator Business

    I think the creator business is probably a little confused about itself, about where the edges of what’s a creator business can be found, but that’s fine.

    What I like about the general concept is how most creators have some important web and online needs in common. Most other businesses have at least some parts of the same set of needs, but the scale and interconnection of the tools used to address those needs is actually fairly different.

    It’s hard to imagine a creator business getting much use out of Salesforce (though no doubt somebody’s about to tell me otherwise). It’s too complex, requires too much interaction on a per client or per prospect basis, and so on.

    Alright, maybe there’s even an argument to be made that Salesforce could make sense when dealing with the actual thousand true fans if that’s the way you’re thinking about what you’re doing. But even there I don’t really see it.

    Home is where your home page is

    You need a web home. You need your own mailing list of prospects and followers. You need a mailing service to get email campaigns sent out, possibly you need a drip campaign type of capability.

    You need to be able to keep the books, possibly you need to track inventory, possibly you need to generate invoices. But you don’t, most likely, need the whole kit and kaboodle of a full-blown accounting package (even one targeting small businesses like QuickBooks). You may wind up using some of the more conventional tools, but you’ll just be using the outside edge of what they can do (which includes all the things you don’t need).

    You may have paid subscriber needs, or you might want to be supported by a more Patreon-like (pay by the work product item, for instance) approach, and so on.

    Build by plugging things together

    What you need is a sensible platform where you can maintain your own web presence and, ideally, layer on the tools you need in a way that keeps things minimal and manageable by a solo operator or a small team. Almost all the things you need to do can be handled by a seeming universe of SaaS operations with annual subscriptions, but it you’re not careful you wind up paying a big stack of monthly fees for things that you wind up figuring out how to interconnect into a system on your own.

    While PeakZebra’s initial product vision wasn’t targeted specifically at creators, it’s use in the creator economy became increasingly obvious as we moved forward building our toolset. You want newsletter signups, but not lots of extra baggage managing your lists. You’d like interactions with users that let you learn more about them individually, but in a way that allows you to mass customize the content you present each one.

    You need subscriptions? We do it by harnessing one of the most-used WordPress plugin options (but you see it as part of our offering–no setting up, configuring, and learning to navigate completely new systems). You need reminders sent to members whose subscriptions will expire soon? It’s in there and it’s dead simple.

    We’ve got some things to add before this makes total sense as a use case for PeakZebra, but we’re well on the way, so if you’re a creator, you might want keep an eye on us.

  • A Modest Licensing Proposal

    Hey OSS folk: we need to start thinking outside the conventional GPL licensing box. We need a rational ecosystem for paid plugins and themes (in WordPress) and analogous capabilities on other OSS platforms. I think we can create a much better arrangement for all involved.

    I envision a licensing system that allows participation in an overarching governance system. If you want to take part in a project that uses this system and use its associated .org repositories and so on, you’d get a license for any internet-facing deployment. You’d pay a small licensing fee, let’s say five bucks a year, calibrated downward as needed to take differences in international buying power into account. Five bucks gets you a vote. For some number of sites, let’s say two dozen, a single five buck payment would get it done, but for big players, measured by sensible metrics but I’m not sure which, more cash would be involved. And bigger players would have more (but not infinitely more) votes.

    It’s a board!

    The main task for voters would be selecting a three or five-person board, but for really large issues (classic editor or blocks, to take an example from the past), direct votes might be called.

    In a greenfield new project, I’d propose a mechanism where the project creator got a big block of votes, such that they could be benevolent dictator for a while (I think there’s value in this, early on—clarity of vision and so on), but as popularity of the project grew and more people acquired licenses, the control would naturally and gradually shift over to the community as a whole.

    Who runs the thing that run the things?

    Who would run this? I imagine a few pillars of the OSS world forming some legal entity that maintained the license sales and voting procedures. It would be possible, maybe even preferable, to run this on a purpose-built blockchain, but it’s hardly a requirement, as the community would just need to trust whoever was running it.

    Now then, what about the money? All the things you might expect the money could be used for would be what the money was indeed used for, at the discretion of the elected board. And we’re talking about a substantial amount of money, the kind of money that drives early marketing campaigns, but clearly also full-on development in the form of sponsorship for core developers and, well, other things like a project’s training videos on YouTube…all the things.

    Not GPL

    To make it work, we’d need to step away from GPL licensing. That sounds severe and even ill-advised, but just because something isn’t running under a GPL or MIT license doesn’t in the least mean it can’t be open source.

    With a non-GPL license, we could have the benefits of open source, but create ways for companies writing plugins and the like genuinely to keep control over how their code was used. The code could remain open, or mostly open, but no one would be in a position to simply take code and sell it as their own (something GPL expressly allows). If the takeover of ACF as SCF didn’t feel right to you, this is how to fix the rules that make it possible.

    If we went this route, selling licenses for use of the governance services of new OSS projects, we could deal with problems such as though currently creating havoc in the WordPress community in a straight-up fashion. Under this kind of arrangement, Matt Mullenweg would have migrated out of a BDFL role years ago. We can’t undo the WordPress GPL license (if we even wanted to), but we could avoid wasting our time wringing our hands and making squeally noises. That’s right: squeally noises.

  • Imagining a WordPress Greenfield

    Just suppose, just for a moment, that you and I were tasked with creating something every bit as wonderful as the wonderful parts of WordPress, but starting with a (mostly) blank slate.

    It’s the plugins

    Well, one thing we have to get out of the way right away is what to do about the huge number of useful plugins that are the particular strength of the WordPress platform. Do we want to walk away from the ecommerce plugins, the learning management kits, the membership tools?

    If we want our cake and the eating of it, we’ll have to reckon with the immutable fact that all of that stuff is written in PHP and it all runs on the server. There are no lambda plugins. There are no client-side plugins.

    And it’s PHP. Some folks argue that PHP is antiquated, but I think that’s about fashion more than sense. It’s a fairly sophisticated language, performant, all that.

    All JavaScript?

    But as long as we’re declaring a fresh start, you’ve got to reckon with the hard truth that PHP doesn’t run on clients. Essentially, JavaScript is the only choice in that regard. And if you’re running JavaScript on the client, it makes life a lot easier to be running the same language up on the server.

    If we change gears and use Node.js on the server, then the challenge is finding some way to continue using existing plugins.

    I’ve turned this over and over in my mind. On the one hand, it seems pretty likely that a WordPress-specific translator could be built to turn plugins in Node.js plugins. If we do that, though, then we have to support all the action and filter hooks.

    So, on the other hand, maybe what we want is to make it easy for plugin providers to rewrite their code bases anew. Assuming an approach that was generally similar to WordPress’s approach, developers would have a pretty good sense of how they should approach various tasks.

    For example, you’d probably want get_current_user() to be getCurrentUser() and you’d probably want it to return a user ID. And you’d want to have roles and the roles would be collections of capabilities.

    Post much?

    That’s easy enough (maybe), but do we really want to commit to preserving the concept of everything being a post? We’d have to give that one some thought, but maybe the easiest way forward is to stick with posts and pages and custom posts and such.

    There’s a lot of complexity in core, though, the inevitable result of organic growth over twenty years, and maybe we just carefully sort through and discard a lot of the baggage that still works but probably was never such a great idea. And maybe we tack on a new concept or two, like having a React-like routing system be the default.

    But what we want is for WooCommerce defectors to have a straightforward sense of how to write a new and significantly more straightforward ecommerce solution. NewCommerce?

    Astro adoption?

    There’s another question to consider: given the complexity of building this sort of system, possibly the best way forward is to start with an existing system and then add on whatever stuff is needed to support critical plugin rewrites.

    This is the line of thought that has landed me at the front door of the Astro community. If you’re unfamiliar with Astro, it does content sites, like our friend WordPress. And it’s server centric, like WordPress. But it’s also vastly newer and thus not yet covered in all those rustic barnacles.

    It appears to have themes. It doesn’t, as far as I can tell, have plugins in the sense one has them in WordPress, but it seems at least conceivable that an interface to a plugin system could be created. It doesn’t have an in-built editor, but again, that seems like something that could be whipped up. Or, in a funny little twist, I feel pretty darned confident that the WordPress block editor could be pressed into service.

    So I’m going to explore Astro. Not because I’m so convinced that the current troubles in the WordPress world are going to lead to WordPress falling apart, but because I think we all need to think about hedging our bets. And, frankly, because there may be better options out there in the blog-and-content-creation universe.

    So, more to come on this. One clear takeaway, though, is that WordPress has built up an enormous and enormously useful ecosystem and feature set over the years. Leaving would be painful.

  • Headless WordPress and why it matters

    You know there’s headless WordPress, but may not be clear on how you’d make it happen. Or, more importantly, why you’d make it happen.

    What is headless WordPress?

    Let’s start with a quick rundown of what makes a WordPress site headless, why the naming in this case is exactly backwards, and just generally get ourselves on the same page.

    The conventional headful approach

    Normal WordPress is a world in which the action happens on the server. A website visitor requests a page from the server and the server assembles the page components (header, body, footer) from the database and any relevant templates. This is sent to the browser, any browser at all. And if something happens down there on the browser, it will result in a new page being requested from the server.

    Where this basic operation is perhaps most clearly visible is when the site provides some kind of data application. Maybe it’s a CRM application, so you might request a list of clients in the system. You get a display of the first 25 of them from the server, say. If you want to see the next page of clients, a new page will be requested from the server. If you want to see a particular client, a new page will be requested to display that client’s information. If you change the information for that client and want to save it, you’ll submit a form to the server and a new page will be delivered to show the update.

    Meanwhile, in the rest of the universe

    For most of the rest of the web, this isn’t typically how an application works, however. If you start with an application that shows a list of client records, then when you want to see the next page of them, a request will be sent to the server to retrieve only the data for the clients that need to be shown. The page with the client list won’t be replaced; rather, the new set of clients will be displayed on the existing page where the previous clients were listed.

    It’s possible that you can edit any of the client fields you can see on each row of the listing. Let’s say you do this and press a save icon at the end of the row you’ve changed. Again, this doesn’t result in a new page being requested. Instead, the listing continues to show the change you made and the change is sent as an update request to the server.

    The server, in other words, is just supplying data at this point, not pages (though, in our scenario, it probably supplied the initial listing page).

    Decouple this

    There are two things we should notice about this scenario. First, there’s got to be some kind of back end that answers requests for data and updates, even if it’s not supplying the pages. Second, the pages still have to come from somewhere. But the pages and the data don’t really have to come from the same place, and thus we can say that the presentation and the data have been decoupled.

    When you decouple the head of a thing, well, it becomes headless. So the baseline idea of headless WordPress is that there’s a WordPress server running, but it’s not supplying the pages that the website visitor is seeing.

    So where are the pages coming from? That depends, but most scenarios out there on the web right now fall into the basic pattern of using React (or some React framework that extends React) to create pages that can be retrieved from web servers as plain HTML and JavaScript files. These pages aren’t assembled or calculated on the server end, they are simply sent to the client as they stand. You’ll hear these scenarios referred to as static sites. That’s because the server doesn’t muck around with them–they can be plenty active once they are displayed in a browser window.

    One question that may already have popped into your mind is: what is React? And that’s an excellent question, but not one that we’re going to answer in any detail here. Suffice it to say, it’s a pre-built set of capabilities implemented in JavaScript, where the capabilities mostly have to do with user interactions.

    The key thing is that JavaScript and React are capable of asking the server (or more than one server) for data that it needs to display. The server that sends the data down to the browser in the headless WordPress scenario is, you guessed it, a WordPress server.

    Headless

    There are plenty of headless scenarios where the server isn’t a WordPress server and there are even scenarios where there arguably isn’t a server in the traditional sense.

    But we’re talking WordPress here. In that scenario, there are two primary ways that WordPress might interact with whatever’s going on down there at the browser window. It may, in the older and more widely adopted approach, use a REST API to make requests for data (or requests to place or update data on the server). Making a REST call is based on requesting a particular URL and it either places any changeable data at the end of the URL (as parameters) or it arranges them in the same way you might arrange data when posting a form to a web server.

    The other approach out there these days involves using a Graphql interface. This is more like opening a window directly into a database and making queries. The details of this don’t much matter for this discussion, the point is that it’s possible to install a plugin that creates a Graphql access point for a WordPress site.

    Wait, but why?

    Why would you take this headless approach, though?

    The obvious first answer is that it enables you to have a different language and framework running on the client side of things. If you want a React application that serves up a lot of server-side content, using WordPress as your CMS might very well make sense.

    Additionally, though, it gives you the capability to render and rerender a page in sections, so that you aren’t necessarily requesting a whole new page from the server every time anything happens.

    Now, as it happens, you can pull off this same trick using the new Interactivity API in WordPress, because it makes the front end capable of doing various things on its own. It let’s you build a “headless-seeming” user experience completely within a WordPress context.

    It’s not clear yet how well the Interactivity API will fare, as it’s still relatively early days, but it’s an interesting option for dynamic front ends (plus it’s in use within WordPress core, so it’s not likely to go anywhere anytime soon).

    WordPress makes a pretty solid CMS, particularly where the content is of the human-readable sort.


  • Creating a Block with Cursor AI

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

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

    Keep it simple

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

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

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

    Only too happy to oblige

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

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

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

    Keep your context ungoofed

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

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

    Doublechecking

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

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

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

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

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

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

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

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

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

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

    More reports from the coding trenches to come…

  • World’s Simplest CRM

    Here’s a use case for PeakZebra that I think makes all the sense in the world.

    We begin with the thought that most of us actually don’t need lead scoring automation.

    Just the facts

    You do need to be able to store information about your prospects. And if they they become customers, you need to make a note of this, either within the same database or by transferring them to some other system. And you need to track their payments and so on.

    For a lot of companies, there’s not actually all that much distance between prospect and customer. For instance, someone contacts you to ask for details on the professional service you offer. You talk with them, gather some info. Maybe you make them a written proposal. They accept the proposal or they don’t, but even if they don’t they aren’t, should some future opportunity lead to a new proposal, exactly a run-of-the-mill prospect anymore.

    Whether a potential customer or an actual customer, you need their contact information and you need to keep a record of your interactions with them. If they send you an email with a pre-sales question, you want to keep a copy. If you give them a Zoom tour of the product, you want to have a record of when and what they thought about it.

    You need to be able to track any deliverables you’ve promised and you need to be able to set reminders for future follow up.

    If you’re really on your game, you can also be learning about them as they interact with your web site and you can build up a segmentation model for your customers (and your prospects, which is arguably almost more important). That work happens on your customer-facing websites, of course, but PeakZebra supplies some great tools for that.

    Not Rocket Science

    The key point here, though, is that none of this is actually complicated. Salesforce will do the job, but you’re paying for the whole lot more that it can also do. Like lead scoring. Even at the small business entry level version, this is $25 per user per month. The next step up is $100 per user per month.

    The PeakZebra approach is dead simple. There’s a “Person” grid that shows contacts. If you want, you can easily have different grids for prospects and clients. If you click “Add a New Person”, you get a form for that.

    Out of the box, you can associate each contact with a particular internal team member. You can tag each person with whatever tags you like (you create them), and you can make a note of each interaction you have with them over time.

    Simple, then customized

    And that’s it. Unless you need something else. In some cases, you can simply add on things you want, if that’s your preference. But you can also queue a request for the change. We generally deliver them within two business days and at a very low cost compared to hiring a conventional developer to jump in cold.

    You can learn more about the pricing model for this sort of change by signing up for our occasional email updates.

  • Is the WordPress Plugin Economy Played Out?

    People have made some very impressive business successes by offering non-free plugins and plugins that are extensions to other plugins that are big enough to have extensions of their own (WooCommerce being the primary example). But I think it’s increasingly unlikely that this is a good way to launch a successful business.

    [Since originally writing this, it’s also occurred to me that the agency approach to making a living in the WordPress space may also be living on borrowed time. That’s because high-end website owners and their agencies may opt to avoid the perceived risk of WordPress coming unglued and may in any case prefer different architectures while, at the same time, lower-end sites (blogs for authors and the like) are increasingly very well served by “drag and drop” sorts of offerings. I’m thinking of website builder SaaS offerings, certainly, but also platforms like Substack and Patreon. If my goal was to create a subscription newsletter, I wouldn’t presently choose a WordPress site if I wasn’t already very familiar with WordPress.]

    I’m thinking primarily of bootstrapped businesses, but I also suspect that bootstrapped startups may be all there is in the WordPress space, at least for a while, because Matt Mullenweg’s takeover of Advanced Custom Fields (and his subsequent takeover of parts of the premium version of ACF) has, I’d imagine, significantly cooled the enthusiasm of venture capital investors for premium plugins.

    In the bootstrap world, there’s probably less reason to fear that some Matt out there will steal your goodies, but that doesn’t mean someone else couldn’t. The ACF heist was a poignant reminder that you really don’t own your own code in the world of open source (which, come on, was the whole point of open source in the first place, so don’t act shocked).

    As I write this, there’s more of a serious movement afoot to revise WordPress governance so that it’s more of a community driven project (instead of a one-guy project that has a huge community). But who knows how that will play out.

    SaaS as a solution

    As I’ve mentioned elsewhere, one approach that protects whatever secret sauce you bring to the meal is keeping the sauce on the server side of a SaaS offering. For a lot of plugin builders, the trick will be simply not to package the plugin as a plugin.

    But I think there’s another issue, and I even think it’s one of the inherent strengths of open source as a concept: over time, free versions of things get better and better. You may have a feature-laden free version and a whole bunch of great goodies to hold in reserve for the premium version, but over time you’ve going to see other developers make competing products that offer some of your premium features for free.

    Freemium limps along

    I don’t mean to say this kills the freemium model, only that the tendency is toward making the premium side of the equation less valuable over time. Given that pricing in the WordPress ecosystem is arguably too low, this seems likely to make would-be entrepreneurs not exactly thrilled to jump in with both feet.

    This is too bad, because there’s nothing quite like the WordPress plugin ecosystem (or the theme system, for that matter) out there in the software world, and it’s offered a way for a lot of smaller businesses to get a foothold and make a go of it. Not to mention creating ways to create all sorts of non-business blogs and the like.

    But I think this shrinking premium benefit phenomenon is inherent in current-day WordPress and open source. There’s less and less incentive to spend a year rolling out a paid plugin. If you win, you don’t necessarily win much, your potential winnings aren’t secure, and competition from free alternatives is going to slowly erode your niche.

    Will the wolf survive?

    So do people like me stick with WordPress? Well, if we’re specifically looking at my case, then yes. I stay in WordPress, trying to leverage opportunities that take advantage of the platform’s enormous market share without getting bitten by the current growing risks to WordPress business.

    (I will say I’m tempted to just switch up and go full time on whatever Joost and company wind up concocting–but that too will be WordPress in some form or fashion.)

    I’ve got to believe that when a framework on the Internet has 40% market share, there’s still lots of opportunity to supply things people need, even if it’s a smooth, easily understood offramp from WordPress.

    All that said, though, I’m still inclined answer the question I started with–is the plugin business played out?–by conceding that, yes, it may well be.

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