Category: Build in Public

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

  • Naming the Business Model

    First and foremost, PeakZebra is a SaaS offering. It’s the familiar online model, where you pay monthly, except maybe I’ll do what it seems like everyone else is doing these days and force you to pay for a year (which, even if I wind up doing it, I hate).

    But there’s a second layer, where you can, if you choose, pay for individual tweaks to the applications you’re using (well, tweaks right up, theoretically at least, to wholesale creation of full-blown new apps). The model here is what’s been called “productized services,” which is an odd name but one that does manage to convey a sense of the value proposition, which is that you pay a controlled and fixed amount for your use of what otherwise might have been charged on a project or hourly basis.

    Agency?

    The “thing” you get from PeakZebra is a customizable website, so another way of thinking about this is to call it an agency. But it’s really not an agency in the traditional sense where you show up and request a customer-facing site that represents and explains your business to the internet.

    Why do I care what to call the business model? I only care because I want a quick way to communicate what’s on offer. “Internal tool agency” doesn’t do it for a few reasons, I’d say.

    Oh, and it seems obligatory that I convey the idea that AI is involved, because that seems to be the only thing that anyone is paying attention to in new products and services these days. I noticed just now that Airtable’s H1 is now “Digital Operations for the AI Era.” To be honest, this makes me instantly just the tiniest bit suspicious. And to be fair, it’s not like I even know what it is that they’re using AI for.

    AI Magic Broker?

    The irony is that I do absolutely plan to be using AI behind the scenes (as per several blogposts by now), but I’m actually more inclined to be relatively quiet about it. It’ll make things fasters for developers handling requests, but the key point is that there’s an actual developer involved.

    So maybe I focus on it being a SaaS for ad-hoc apps, but a subscription that makes it seamless and easy to mold the app to your needs. If you have a real management job already, you might very well be not even the least bit interested in spending time learning how to do this and that in Airtable or Notion (or pick some other preferred magic no-code SaaS).

    Maybe “guided low-code automation at a fixed subscription price.” OK, that’s too long, so maybe “subscription app refinement.”

    Low-code Whatever

    Actually, I hate that one. I still have some noodling to do on this, I guess. I think getting it right will give me greater clarity when I try to tell people what the “thing” is that should grab their attention about PeakZebra. Sure, it’s a way to pay for the world’s least-featured CRM, but there’s a little more to it than that.

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

  • A Real-World Example of Avoiding Unneeded Prelaunch Dev

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

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

    Extra work for fun and profit

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

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

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

    Admin can wait

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

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

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

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

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

  • AI Coding and Context

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

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

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

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

    how can it not know what it is?

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • What WordPress Should Be

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

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

    Hammer time

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

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

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

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

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

    Triumphant return?

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

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

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

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

    Clarity

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

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

    — added 2/5/2025

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

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

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

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

    —-

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

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

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

  • Salute Your Cursor AI Overlords

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

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

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

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

    Cursor buzz

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

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

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

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

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

    Cursory beginnings

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

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

    WordPress Knowledge?

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

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

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

    Working in Ignorance

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

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

    For example

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

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

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

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

    help-text-block.php

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

    and block.js

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

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

    Does it dream of electric sheep?

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

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

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

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

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

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

  • Client variation management

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

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

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

    Three options + 1

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

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

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

    Just the code

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

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

    Don’t touch core

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

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

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

    Only change what you can make trackable

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

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

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

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

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

  • Slots to Slotfill, Callbacks to Call

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

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

    Single file of truth

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

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

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

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

    But which callbacks?

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

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

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

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