So, sure: WordPress is a CMS. I think it’s sometimes the answer given as a defense against the cynical question: “What’s WordPress still good for these days?”
It’s a pretty great CMS, honestly. But what if we don’t focus on the CMS aspect so much. I think there are more imaginative ways to think about the opportunities WordPress provides.
Because beneath the templating and posts and menus, WordPress is something far more powerful: it’s a runtime (and by runtime I mean the sense of the word that conveys framework or platform). I tweeted this thought yesterday but, alas, I had more thoughts on the subject, so today we’re blogging.
WordPress has a built-in database, user authentication, routing, a REST API, and the ability to conditionally load resources or change output based on server state or user state. There’s all the makin’s of application logic in there.
Please Embrace the Today’s WordPress, Please
The problem is, most WordPress sites still treat it like it’s 2011. We build themes, add a few forms, maybe install a member plugin—and that’s it. (Not that there’s anything wrong with member plugins–the PeakZebra site uses one.)
A WordPress site gives you a pretty solid authentication setup, one that’s been battled tested on a ridiculously large set of sites in the wild. The fact that WordPress sites get compromised sometimes (though, to be honest, this generally isn’t about the way authentication works in WP, but is either password guessing or other problems (insecure plugins) that have nothing to do with authentication.
Want to add two-factor authentication? People have already packaged that up in plugins.
Or consider API’s. Yes, it’s cool that the CMS side of WordPress offers full access via REST, but I’d say it’s even cooler that it gives you a framework where it’s very quick and easy to add as many other application-specific REST endpoints as you’d like. You can make them public, but you can also make them private and, hey presto, WordPress takes care of the security so you don’t have to.
If you start thinking of WordPress as the foundation for an actual application, a whole world opens up: dynamic interfaces, persistent user flows, personalized content, and client-specific tools—without abandoning the platform you already know.
WordPress isn’t “just a CMS.” It’s a bigger deal than that.
Since the stuff I’m building these days has similarities to some of the things Airtable does, I figured why not build a version of the request queue I’ve built with PeakZebra, only with Airtable.
TLDR: Throw in all the AI you want, once you’re past just storing everything in a single sheet, it’s pretty easy to make a false step (or use the AI to try and do too much) and you wind up with something that just doesn’t work. And it’s possible that it won’t be obvious to you that it’s not working like you think.
Airtable is pretty cool and there’s no question that it’s got way more polish than PeakZebra does just now, but then again, there are ways in which it’s considerably trickier to build things with Airtable if you’re coming in cold.
Magic AI bullshit
These days, it almost goes without saying, Airtable has some magic AI bullshit built in. The promise is that you describe the app you want and it builds it.
Honest to god, I didn’t try to trick it into screwing things up. I tried to come up with a concise description of what I wanted my request queue to do. I’m afraid I didn’t preserve the actual prompt I gave it, but I can tell you that it created tables that appeared to do the right things, but the relationships between them weren’t at all correct.
So the AI stuff was a bit of a crock, but that’s not really what I was interested in getting at in any case. And if you just wade in and do it yourself, there are all sorts of things Airtable does that are very powerful.
Easy and powerful things
It’s very easy, for example, to connect a field in one table to records in another table. For instance, if you have a table where you store all the requests that are coming into your request system, it’s easy to tell it that the client field should come from the clients stored in a client table.
And there are some nice touches. When you click on the client you’ve chosen in one of the request records, it pops up a view of all the data in that client’s client record. Handy.
So the app let’s you establish clients and let’s you create a form for adding requests that are associated with your client identity.
Twist once for death
I wanted an extra twist, though. I wanted to be able to let a client create a bunch of associated requests–all the tasks needed to carry out a given project–but not necessarily put all of those tasks into the request queue.
So I wanted a project table to store the name of various projects, plus a tasks table to hold all the tasks associated with all the projects of all the clients.
This is easy enough. It’s also straightforward to add a field that toggles to tell you whether a given task is currently in the request queue.
Doing this, though, means that you don’t want to store requests in a request table, but that instead you want the request queue to be a view of the tasks table that filters on tasks that are flagged as being in the queue. You can also add a further filter or grouping to show the queued items by project and/or by client.
Wait, one more twist
Ah, but another twist. Some things are projects with a bunch of different steps, each one handled as a separate task. But other things are one-off things that need to be done and that really aren’t part of a task. Say you’ve got a website and you want to request a change in some content on that website. That’s not a project.
You could create a project for “non-project” tasks, but what’s conceptually cleaner is to have things in the queue either be tasks from projects or be standalone tasks. There’s no obvious way to have a field be populated by data from either of two tables.
Now, let’s be clear. I don’t have any doubt whatsoever that there’s a way to do this. There’s almost certainly several different ways to approach it. But if you’re trying to avoid learning lots of technical minutia about the Airtable environment, you’ll hit a wall with something like this.
Sometimes a little code is the magic
The PeakZebra approach doesn’t preclude figuring out how to do the trickier stuff yourself, but as part of the basic arrangement you also have the option of just, well, using the PeakZebra request queue (on PeakZebra, not Airtable) and just asking us to do it for you.
But my real point here, I think, is that services like Airtable and Notion and others are focused on making everything work for everybody without requiring any code. And this can sometimes completely obscure the ease with which the same thing could be accomplished in a “low-code” approach with just a couple of lines of code.
And oh by the way: I’m not as against AI for coding (and similar code-like tasks) as it might sound like in this post. I’ve been doing more and more AI assisted programming of late, and there are definitely things about it that make me loads more productive.
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.
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…
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…
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.
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:
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.)
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 '';
}
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.
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.
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.
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.
One thing about the strategy where you hedge your WordPress bets by offering your wares as a SaaS built on WordPress is that it opens up the question of what’s in your SaaS.
Once you’ve created a setup where your SaaS customers are interacting with your WordPress server, a potential next step is to incorporate some third-party package into your build and offer access to that as part of your service.
Anticipated?
Some of the more complex plugins out there absolutely anticipate this sort of use and have options that let you license the plugin accordingly. Others, less so, and it’s also worth considering that nothing says you can’t take free plugins and offer them wrapped up in your service in the same way.
It’s easy to see how you might end up with a scenario where you’re wrapping up a paid plugin with a lifetime, unlimited license that doesn’t cost very much and then reselling it heavily. It’s the kind of thing that makes you wonder whether you’re taking advantage, but then again, the unlimited licensing is largely targeting agencies who are reselling the product at a markup, just not necessarily in SaaS packaging.
By and large, I think it’s legit (though I’m open to counterarguments). If there’s an unlimited license on offer, then SaaS resale seems entirely above-board, from a licensing perspective. And, for better or worse, there’s the fact that GPL licensing pretty clearly requires that you let people take your code and resell it if they want, just as long as they credit you and retain the same licensing.
The admin side
One potential problem with a SaaS setup is that your end users may well need at least some access to WordPress admin functions. But you don’t really want them having to learn their way around the back end, probably don’t want them poking around in there in any case, but on the other hand you probably don’t want to have to write a front-end version of the admin functions.
For this, though, there are a couple of plugins that expose some or all of the back-end interface to front-end users.
The one that I’m using for this particular need (for the time being, at least) is WPFrontend Admin. It enables you to create front-end pages like this one, where the guts of the user admin interface are nicely nestled inside a front-end page:
It’s a clunky design, just like it is on the back end, but you can pretty it up a bit by overriding various CSS settings. I haven’t really dug into that yet, but what this gives PeakZebra is a way to allow clients with the right access privileges to manage the user’s they’re allowing on their instance of the PeakZebra service.
PeakZebra users
As it happens, I’ve also written a spinoff of the PeakZebra form blocks that specifically map to WordPress user account fields (and meta) and that are specifically for use on the front end. They’re implemented using the Interactivity API which means they will at some point confer special, magical powers.
But implementing a full copy of the user admin interface at this point in PeakZebra’s development seems like a pretty significant detour, given that I can just surface the user admin functionality on the front end using the plugin.
The thing about the Frontend Admin plugin, though, is that it’s expressly built for the scenario where you resell a complex plugin. Let’s say you take the Groundhogg CRM. It basically lives on the admin side of the site where you install it. But you can make all those pages magically available on the front end with the Frontend Admin.
And this trick works with all sorts of plugins. Check out their list of common applications used this way.
I don’t think this is the future of WordPress, exactly. But we’re going to see more cases where people are using this sort of setup to keep their own modifications hidden on the server side of things. Whether this is a good thing is debatable, but I think overall it’s a good thing to have a business ecology around the core open-source WordPress project, and the status quo of how business exists in the WordPress world is clearly being seriously re-evaluated right now.