[{"data":1,"prerenderedAt":1188},["ShallowReactive",2],{"gate-notes":3,"layout-site":664,"layout-footer":686,"gate-uses":694,"gate-gallery":853,"journey":868,"journey-projects":1052},[4,218,457,554],{"id":5,"title":6,"body":7,"cover":197,"date":202,"description":203,"draft":204,"extension":205,"meta":206,"navigation":207,"path":208,"placeholder":204,"readingMinutes":209,"seo":210,"stem":211,"summary":212,"tags":213,"__hash__":217},"blog\u002Fblog\u002Fcleaning-a-hacked-wordpress-site.md","Cleaning a hacked WordPress site, and the 27 minutes I got wrong",{"type":8,"value":9,"toc":186},"minimark",[10,15,19,22,25,29,32,79,87,91,94,97,100,103,106,109,129,133,136,139,142,145,156,159,162,166,173,177,180,183],[11,12,14],"h2",{"id":13},"what-visitors-were-seeing","What visitors were seeing",[16,17,18],"p",{},"The first sign of trouble wasn't spam or a warning from Google. Visitors to a\nlong-running WordPress review site started getting what looked like a Cloudflare\n\"verify you are human\" page, except there was no checkbox. It told them to press\nWin+R, paste in a command, and hit Enter. Apparently, proving you're human now\nrequires Windows and a complete lack of suspicion.",[16,20,21],{},"That's a ClickFix attack, and it goes after the people reading the site, not the\nsite itself. Anyone who followed those steps would have run the attacker's command\non their own computer.",[16,23,24],{},"The site is hosted on Kinsta. The client needed me to find everything the attacker\nhad left behind, get rid of it without losing any real content, and put the site\nback online in a state they could trust.",[11,26,28],{"id":27},"what-i-found","What I found",[16,30,31],{},"The fake check page was just the part people could see. Behind it were:",[33,34,35,43,49,55,61,67,73],"ul",{},[36,37,38,42],"li",{},[39,40,41],"strong",{},"Five must-use plugins"," dressed up as caching and \"site guard\" helpers. MU\nplugins load on every request and don't show up in the regular plugin list, so\nthey're a great place to hide. They did guard something. It just wasn't the site.",[36,44,45,48],{},[39,46,47],{},"Four plugin folders on disk"," that never appeared in wp-admin.",[36,50,51,54],{},[39,52,53],{},"Small backdoors"," that took an encoded command from a POST field and ran it.",[36,56,57,60],{},[39,58,59],{},"A spam API"," in the site root that a remote script could call to publish posts.\nEven spammers have a content workflow now.",[36,62,63,66],{},[39,64,65],{},"1,224 casino and betting posts"," in several languages, all published through\nthat API. Say what you want about the attacker, but they took\ninternationalization seriously.",[36,68,69,72],{},[39,70,71],{},"Settings and scheduled jobs"," that kept everything running.",[36,74,75,78],{},[39,76,77],{},"An administrator account the attacker was using",", plus Application Passwords,\nwhich give API access without going through the normal login.",[16,80,81,82,86],{},"The code injecting the fake page was in the child theme's ",[83,84,85],"code",{},"functions.php",", tacked\non below 25 lines of legitimate code. A plugin vulnerability had nothing to do with\nit. There was no zero-day and no hooded genius typing green text in a dark room.\nAccording to the logs, the attack started a month earlier with a valid\nadministrator login. Hacking gets a lot easier when you have the password.",[11,88,90],{"id":89},"doing-things-in-the-right-order","Doing things in the right order",[16,92,93],{},"Most of the work was WP-CLI over SSH, along with Kinsta's dashboard and logs. The\ntools were nothing special. What mattered was the order I did things in.",[16,95,96],{},"First, I cut off access. Before I changed a single file, I put the site behind\npassword protection and blocked the attacker's IP addresses. I had to turn off the\nCDN and edge caching first, because Kinsta won't password-protect a site while a\ncached copy can still be served. Once that was done, visitors stopped getting the\nfake page, and the attacker got to find out what being locked out of somebody\nelse's site feels like.",[16,98,99],{},"Next, I saved the evidence. The malicious files and a full database dump went into\na private folder outside the web root. Whenever a question came up later, like when\nthis all started or whether a certain post was ours or theirs, that archive had the\nanswer.",[16,101,102],{},"Then I made sure I could undo everything, which I learned the hard way. On my first\npass, I permanently deleted all 1,224 spam posts. The client had expected them to\ngo to the Trash so anything real that got swept up could be restored, and they were\nright to expect that. I restored all 1,224 from the database dump and moved them to\nthe Trash instead. After that, nothing got removed for good until the client signed\noff on it.",[16,104,105],{},"I also reinstalled instead of trying to repair anything. WordPress core passed\nchecksum verification. Every free plugin got replaced with a clean copy from\nwordpress.org. The premium plugins came from the client's other sites, which\nweren't affected, and never from the compromised backup, because restoring the\nattacker's handiwork seemed counterproductive. I added those one at a\ntime and checked the front end after each one.",[16,107,108],{},"Hardening came last:",[33,110,111,114,120,123,126],{},[36,112,113],{},"new salts and a new database password",[36,115,116,117],{},"file editing disabled in ",[83,118,119],{},"wp-config.php",[36,121,122],{},"Application Passwords turned off with a two-line MU plugin (it turns out they can\nbe used for good, too)",[36,124,125],{},"Wordfence for scanning and brute-force protection",[36,127,128],{},"a review of every user account, including the attacker's, which has been let go\nwithout severance",[11,130,132],{"id":131},"the-27-minutes","The 27 minutes",[16,134,135],{},"Before I took the site public again, I scheduled a log check for one hour later. I\nfigured it would be a formality.",[16,137,138],{},"The access log showed a successful administrator login 27 minutes after the site\nwent public, and another casino post went up seconds later. Whatever else you can\nsay about these people, they're punctual and they don't miss a content deadline.",[16,140,141],{},"The account belonged to the client. It was the one administrator account I hadn't changed the password on\nyet, and whoever got in a month earlier still had that password.",[16,143,144],{},"Fixing it took a few minutes:",[33,146,147,150,153],{},[36,148,149],{},"I moved the post to the Trash.",[36,151,152],{},"I set a new password on that account and ended all of its sessions.",[36,154,155],{},"I gave every other account a new random password too.",[16,157,158],{},"Nothing else had been touched. The next day's logs showed bots still trying the old\nlogin and hitting backdoors that were already gone, and every one of those requests\neither failed or got a 404. It's the nicest rejection letter I've ever sent.",[16,160,161],{},"So my cleanup was thorough, but my checklist for reopening the site wasn't.",[11,163,165],{"id":164},"cleaning-up-google","Cleaning up Google",[16,167,168,169,172],{},"Search Console showed no manual action and no security issues. Google hadn't flagged\nthe spam at all. It had just indexed it, which is about as close as spam gets to a\nfive-star review. I matched every URL Google listed as indexed\nor ranking against the trashed spam posts and filed ",[39,170,171],{},"440 removal requests",". Those\npages return a 404 now, so the removals won't need to be renewed when they expire. I\nalso confirmed that the sitemap listed only real content, 242 URLs in all, and\nresubmitted it.",[11,174,176],{"id":175},"what-id-do-differently","What I'd do differently",[16,178,179],{},"I'd treat changing every password as something that has to be finished before the\nsite comes back up, not something that happens alongside it. I changed the passwords\non the accounts I could see being abused and left one for later, and later turned\nout to be 27 minutes. When an attacker gets in with a real password, you have to\nassume they have every password they could have seen. Change all of them before the\nsite is reachable again.",[16,181,182],{},"I'd also schedule that post-launch log check every single time. That check is the\nonly reason this is a short section in a write-up and not a site that got quietly\nreinfected.",[16,184,185],{},"And to whoever is still calling those backdoors: they're gone, and the 404 is all\nyours.",{"title":187,"searchDepth":188,"depth":188,"links":189},"",3,[190,192,193,194,195,196],{"id":13,"depth":191,"text":14},2,{"id":27,"depth":191,"text":28},{"id":89,"depth":191,"text":90},{"id":131,"depth":191,"text":132},{"id":164,"depth":191,"text":165},{"id":175,"depth":191,"text":176},{"src":198,"alt":199,"width":200,"height":201},"\u002Fimages\u002Fblog\u002Fcleaning-a-hacked-wordpress-site.webp","A case study cover for cleaning a hacked WordPress site: a fake Cloudflare check telling visitors to press Win+R and paste a command, a 27-minute stopwatch, and a site marked as cleaned",1536,864,"2026-11-08","How I cleaned up a hacked WordPress site with 1,224 spam posts and hidden backdoors, and what a log check caught 27 minutes after it went back online.",false,"md",{},true,"\u002Fblog\u002Fcleaning-a-hacked-wordpress-site",5,{"title":6,"description":203},"blog\u002Fcleaning-a-hacked-wordpress-site","A client's review site was showing visitors a fake Cloudflare check that told them to paste a command into Windows. Getting the malware out took two days. Then the attacker was back 27 minutes after the site went online, and that part was on me.",[214,215,216],"wordpress","security","case-study","pgnikAhi1s2T6HAhzpGfl86LMGlMjujadquQoTpHRWU",{"id":219,"title":220,"body":221,"cover":439,"date":443,"description":444,"draft":204,"extension":205,"meta":445,"navigation":207,"path":446,"placeholder":204,"readingMinutes":447,"seo":448,"stem":449,"summary":450,"tags":451,"__hash__":456},"blog\u002Fblog\u002Fcode-review-is-the-skill-now.md","Code review is the skill now: programming after AI",{"type":8,"value":222,"toc":429},[223,227,247,251,254,257,260,264,267,288,291,300,304,307,310,318,321,350,353,357,360,363,370,373,377,380,383,386,390,393,419,423,426],[11,224,226],{"id":225},"the-short-version","The short version",[33,228,229,235,241],{},[36,230,231,234],{},[39,232,233],{},"AI changed who writes the code."," Planning and implementation now happen with\nAI. Reviewing is still my job.",[36,236,237,240],{},[39,238,239],{},"Accepting AI code without reading it is vibe coding."," It runs, it looks\nright, and nobody has checked it.",[36,242,243,246],{},[39,244,245],{},"How fast you can review correctly is the new measure of a programmer."," Code\nreview is the skill worth training.",[11,248,250],{"id":249},"how-programming-used-to-work","How programming used to work",[16,252,253],{},"For most of my career, programming meant one person holding the whole problem.\nYou read the requirement, worked out the logic, picked the algorithm, decided how\nthe pieces would talk to each other, and then typed every line of it. The thinking\nand the typing were the same job, done by the same person, usually at the same\ntime.",[16,255,256],{},"Skill was easy to see in that model. A good programmer wrote correct code faster\nand needed fewer tries to get there. How much you could produce was how you were\njudged.",[16,258,259],{},"That model is gone from my own work, and I don't think it's coming back.",[11,261,263],{"id":262},"the-ai-workflow-plan-implement-review","The AI workflow: plan, implement, review",[16,265,266],{},"A feature on one of my plugins goes like this now:",[268,269,270,276,282],"ol",{},[36,271,272,275],{},[39,273,274],{},"Planning with AI."," I describe the problem, the constraints, and what already\nexists. We go back and forth on the approach: which hook, which data shape,\nwhat could break for installs that already exist. The plan comes out of that\nconversation. It isn't something I write alone and then hand over.",[36,277,278,281],{},[39,279,280],{},"Implementation by AI."," The code gets written, most of it not by me. Files,\nfunctions, tests, the migration if there is one.",[36,283,284,287],{},[39,285,286],{},"Review by me."," I read all of it and decide what ships.",[16,289,290],{},"The first two steps got dramatically faster. The third one didn't, and it\nshouldn't. It's where the job moved to.",[16,292,293,294,299],{},"A few weeks ago I wrote that ",[295,296,298],"a",{"href":297},"\u002Fblog\u002Fthe-future-of-programming-learn-to-think-not-just-code","programmers should learn to think, not just\ncode",". This is the\npractical side of that. Thinking about systems is how you plan well. Reviewing is\nhow you prove the plan actually got built.",[11,301,303],{"id":302},"accepting-ai-code-isnt-reviewing-it-thats-vibe-coding","Accepting AI code isn't reviewing it: that's vibe coding",[16,305,306],{},"There's a version of this workflow that skips step three. The AI writes it, it\nruns, the page loads, the test is green. Merge.",[16,308,309],{},"That's vibe coding. It isn't a moral failing. It's a description. You're shipping\ncode on the strength of how it feels, because nobody actually read it. For a\nthrowaway script, fine. For a plugin running on someone else's live site, with a\nvisitor's data going through it, it's a bet you're making with other people's\nstuff.",[16,311,312,313,317],{},"The trouble is that AI-written code is very good at ",[314,315,316],"em",{},"looking"," right. It's\nformatted well, the names make sense, the comments sound confident. The mistakes\naren't typos. They're plausible decisions that happen to be wrong, and plausible\nwrong code gets past a skim every time.",[16,319,320],{},"In WordPress work, these are the ones I keep catching:",[33,322,323,330,341,344,347],{},[36,324,325,326,329],{},"A REST route registered with ",[83,327,328],{},"'permission_callback' => '__return_true'","\non an endpoint that writes data. It works in testing because you're logged in\nas admin anyway.",[36,331,332,333,336,337,340],{},"A ",[83,334,335],{},"$wpdb->query()"," with a variable dropped straight into the SQL string instead\nof going through ",[83,338,339],{},"$wpdb->prepare()",".",[36,342,343],{},"Output echoed without escaping, or input saved without sanitizing, because the\nhappy path never sent anything strange.",[36,345,346],{},"A nonce that gets created and never checked.",[36,348,349],{},"A migration that runs on every page load instead of once, because nothing\nrecords that it already ran.",[16,351,352],{},"Every one of those runs. Every one passes a quick manual test. None of them\nannounce themselves. You only find them if you're reading on purpose.",[11,354,356],{"id":355},"review-speed-is-the-new-measure","Review speed is the new measure",[16,358,359],{},"In the old model you could see a programmer's skill in what they wrote. Now you\ncan see it in how fast they can review correctly.",[16,361,362],{},"Both words matter. Fast and wrong is worse than useless, because it gives\nconfidence to code that hasn't earned it. Slow and right still works, but you\nlose most of what AI gave you, and the queue of unreviewed changes piles up.",[16,364,365,366,369],{},"What makes someone fast ",[314,367,368],{},"and"," right is the same thing that used to make someone a\ngood writer of code: they already know what correct looks like. An experienced\nreviewer doesn't read a permission check and wonder whether it matters. They know\nwhere the authorization should be and notice when it's missing. They don't trace\nevery loop from scratch. They recognize the shape and look straight at the edge\ncase the shape usually gets wrong.",[16,371,372],{},"So all the fundamentals still count: logic, data structures, how the platform\nbehaves, security basics. They just get used in a different place. You used to\nspend them typing. Now you spend them reading.",[11,374,376],{"id":375},"reading-for-whats-missing","Reading for what's missing",[16,378,379],{},"The hardest part of reviewing AI code isn't spotting a bad line. It's noticing the\nline that isn't there.",[16,381,382],{},"AI does what you asked, and usually does it thoroughly. What it misses is whatever\nyou didn't think to ask about: what happens on an install that already has old\ndata, what happens when two requests land at once, what happens when the user\nisn't who the code assumes they are, what happens to the thing this change quietly\ndepends on.",[16,384,385],{},"So I read against the plan, not only against the diff. Did it handle the case we\nsaid mattered? Did it touch anything the plan never mentioned? Is there a code\npath we talked about that just isn't in there? The diff only shows you what got\nwritten. The plan tells you what should have been.",[11,387,389],{"id":388},"how-to-get-better-at-code-review","How to get better at code review",[16,391,392],{},"If review is the main skill, it deserves practice, the same way writing code\nused to get practice. What I actually do:",[33,394,395,401,407,413],{},[36,396,397,400],{},[39,398,399],{},"Predict before reading."," Before opening the diff, I guess roughly how I'd have\nbuilt it. Where the real code differs, either it knows something I missed or\nit's wrong. Both are worth knowing.",[36,402,403,406],{},[39,404,405],{},"Keep a list of what AI gets wrong."," The list above started as notes. The same\nmistakes come back, so a written list makes the next review faster.",[36,408,409,412],{},[39,410,411],{},"Read other people's code."," Open-source plugins, pull requests, core itself.\nReading code you didn't commission, with no plan in your head, is the hard\nversion of the exercise.",[36,414,415,418],{},[39,416,417],{},"Still write some things by hand."," Not for speed. So I keep an instinct for\nwhat correct feels like. You can't recognize the right shape if you've\nforgotten how to make it.",[11,420,422],{"id":421},"the-name-on-the-commit","The name on the commit",[16,424,425],{},"I said in that earlier post that even code review will probably need less human\ninvolvement over time. I still think that's where this is heading. But it isn't\nwhere we are now, and right now the review is where responsibility sits. The AI\ndoesn't answer the support ticket. The AI isn't the one a client calls when a form\nstops sending. My name is on the commit, so my reading is what signs it off.",[16,427,428],{},"Writing code got cheap. Knowing whether code is right didn't, and that's the skill\nI'm working on now.",{"title":187,"searchDepth":188,"depth":188,"links":430},[431,432,433,434,435,436,437,438],{"id":225,"depth":191,"text":226},{"id":249,"depth":191,"text":250},{"id":262,"depth":191,"text":263},{"id":302,"depth":191,"text":303},{"id":355,"depth":191,"text":356},{"id":375,"depth":191,"text":376},{"id":388,"depth":191,"text":389},{"id":421,"depth":191,"text":422},{"src":440,"alt":441,"width":200,"height":442},"\u002Fimages\u002Fblog\u002Fcode-review-is-the-skill-now.webp","An AI-assisted workflow of plan, implement, review and ship, with a developer reviewing AI-written WordPress code that flags a missing permission check, SQL injection risk and unescaped output",1024,"2026-09-27","AI writes most of the code now. Accepting it unread is vibe coding. Why code review became the core programming skill, and how I practise it.",{},"\u002Fblog\u002Fcode-review-is-the-skill-now",6,{"title":220,"description":444},"blog\u002Fcode-review-is-the-skill-now","AI now plans with me and writes most of the implementation. The part it hasn't taken is reading that code and deciding whether it ships. Accepting it unread is vibe coding. Reviewing it well, and quickly, is where the programming skill lives now.",[452,453,454,455,214],"ai","code-review","vibe-coding","engineering","9e-9a5kt2HzdQhfpo43HgycHBvnilkMe-tDNbjViyH8",{"id":458,"title":459,"body":460,"cover":541,"date":546,"description":464,"draft":204,"extension":205,"meta":547,"navigation":207,"path":297,"placeholder":204,"readingMinutes":188,"seo":548,"stem":549,"summary":550,"tags":551,"__hash__":553},"blog\u002Fblog\u002Fthe-future-of-programming-learn-to-think-not-just-code.md","The Future of Programming: Learn to Think, Not Just Code",{"type":8,"value":461,"toc":539},[462,465,468,471,474,477,480,483,486,489,492,495,518,521,524,527,530,533,536],[16,463,464],{},"The era of writing every line of code by hand is slowly coming to an end.",[16,466,467],{},"AI can already generate code faster than most developers, and it is becoming\nsurprisingly capable at finding bugs, reviewing implementations, and suggesting\nimprovements. As these systems continue to evolve, even traditional code review\nwill require less human involvement.",[16,469,470],{},"So, does that mean we no longer need to learn programming?",[16,472,473],{},"Quite the opposite.",[16,475,476],{},"We still need to understand programming—but the way we learn it has to change.",[16,478,479],{},"For years, learning programming meant spending countless hours writing syntax,\nmemorizing APIs, debugging errors, and building things line by line. That\napproach made sense when humans were responsible for producing the code.",[16,481,482],{},"But if AI is going to handle much of the implementation, our focus needs to move\nfrom writing code to understanding systems.",[16,484,485],{},"Think about an architect. An architect doesn't need to personally lay every\nbrick, install every pipe, or wire every electrical connection. Their job is to\nunderstand the entire structure—how different parts fit together, where problems\nmight occur, what materials should be used, and how everything should work\ntogether.",[16,487,488],{},"They understand the system so they can make the right decisions.",[16,490,491],{},"The same shift is happening in software development.",[16,493,494],{},"AI may write the implementation, but someone still needs to decide:",[33,496,497,500,503,506,509,512,515],{},[36,498,499],{},"What should be built?",[36,501,502],{},"Why should it be built this way?",[36,504,505],{},"How should the system behave?",[36,507,508],{},"Which architecture makes sense?",[36,510,511],{},"What trade-offs are acceptable?",[36,513,514],{},"How should different components interact?",[36,516,517],{},"What could go wrong?",[16,519,520],{},"That person needs to understand programming deeply enough to make those\ndecisions—even if they aren't personally typing every line.",[16,522,523],{},"In other words, the developer of the future may look less like a traditional\ncoder and more like a technical strategist or team lead.",[16,525,526],{},"So don't stop learning programming.",[16,528,529],{},"Instead, change what you are learning programming for.",[16,531,532],{},"Learn the logic, architecture, systems, patterns, trade-offs, and problem-solving\nbehind the code.",[16,534,535],{},"Because the future may not belong to the people who can write the most code.",[16,537,538],{},"It may belong to the people who understand what code needs to exist—and why.",{"title":187,"searchDepth":188,"depth":188,"links":540},[],{"src":542,"alt":543,"width":544,"height":545,"caption":187},"\u002Fimages\u002Fblog\u002Fthe-future-of-programming-learn-to-think-not-just-code.webp","A developer at a whiteboard of system decisions beside a robot writing code on a laptop, captioned \"not just a coder, a technical strategist\"",1600,900,"2026-09-05",{},{"title":459,"description":464},"blog\u002Fthe-future-of-programming-learn-to-think-not-just-code","AI is getting good enough at implementation that writing every line by hand is ending. That is not a reason to stop learning programming — it is a reason to change what you are learning it for.",[452,552,455],"career","Id7FK6pS1qhGW_lDGNt5ThD6jL1sjywZu573G5SV908",{"id":555,"title":556,"body":557,"cover":651,"date":654,"description":187,"draft":204,"extension":205,"meta":655,"navigation":207,"path":656,"placeholder":204,"readingMinutes":447,"seo":657,"stem":658,"summary":659,"tags":660,"__hash__":663},"blog\u002Fblog\u002Fwhat-renaming-a-plugin-actually-touches.md","What renaming a plugin actually touches",{"type":8,"value":558,"toc":644},[559,563,566,569,572,576,583,613,616,620,623,626,629,633,636,639,641],[11,560,562],{"id":561},"the-second-rename-was-not-my-idea","The second rename was not my idea",[16,564,565],{},"Defer Forms for Contact Form 7 has had two names before this one. The first rename\nwas mine — the old name said nothing about what the plugin did, and its folder name\nbroke a wordpress.org naming rule I'd rather have caught earlier. That one I chose,\non my own schedule.",[16,567,568],{},"The second was not a choice. It went into wordpress.org review under its new name\nand came back rejected: the name led with a generic word and sat too close to an\nexisting plugin's. Review doesn't negotiate. You get a reason and a rename.",[16,570,571],{},"The part worth writing down isn't the naming — plenty has been written about naming\nthings. It's that a rename looks like a one-line change and isn't one, the moment\nthe plugin has been installed anywhere.",[11,573,575],{"id":574},"a-name-is-not-just-a-label","A name is not just a label",[16,577,578,579,582],{},"The visible rename — the string in the plugin header, the text in the admin menu —\ntook an afternoon. Everywhere else the old name was actually ",[314,580,581],{},"encoded"," took four\nmore releases to find and fix, one at a time:",[33,584,585,591,601,607],{},[36,586,587,590],{},[39,588,589],{},"The database table."," Submissions, roles, everything the plugin stores lived\nunder a table prefixed with the old internal short name.",[36,592,593,596,597,600],{},[39,594,595],{},"The developer hooks."," Seven of them, documented, with real signatures — anyone\nwho had already filtered on ",[83,598,599],{},"old_prefix_submission_created"," needed to know it was\nnow something else, not silently stop firing.",[36,602,603,606],{},[39,604,605],{},"The REST namespace."," Every request the builder makes goes through it. Miss one\nreference and the admin looks fine until you open the one screen that calls it.",[36,608,609,612],{},[39,610,611],{},"Post meta keys, script and style handles, admin page URLs."," The last one meant\ntelling people to update their own bookmarks — a genuinely small cost, and still\na cost, and still something I had to remember to say.",[16,614,615],{},"None of this shows up in a diff of the plugin's header comment. All of it is the\nactual rename.",[11,617,619],{"id":618},"the-migration-has-to-run-once-and-only-once","The migration has to run once, and only once",[16,621,622],{},"The database table was the one that mattered most, because it's the one with a\nvisitor's data in it. Renaming a table isn't a decision you make in code and walk\naway from — every install that already has the old table needs something to run,\nonce, that moves its data to the new one before anything tries to read from the new\nname and finds nothing there.",[16,624,625],{},"That migration shipped in the same release as the prefix change. It checked for the\nold table, moved what it found, and got out of the way — the kind of code that is\neasy to write defensively and easy to forget about afterward, because once it's\nshipped it mostly just sits there working correctly for installs that need it and\ndoing nothing for installs that don't.",[16,627,628],{},"Sitting there is not free, though. Every release after that carried one more piece\nof logic whose entire job was reasoning about a table name nothing was going to use\ngoing forward. That's a real, if small, tax — one more thing to read when you're\ntrying to understand what a fresh install actually does, one more thing that could\nin principle misfire against data it was never meant to touch.",[11,630,632],{"id":631},"knowing-when-its-safe-to-delete","Knowing when it's safe to delete",[16,634,635],{},"The migration came out four releases later, and the reason it was safe to remove is\nthe part I'd actually recommend paying attention to: not \"it's probably fine by\nnow,\" but a specific, checkable claim — this plugin has never been distributed\nunder its old internal prefix. Every install anyone could download from that point\non was already past the rename. There was no table for the migration to ever find,\non any install that didn't already exist before the code was written, and there\nnever would be one again.",[16,637,638],{},"That's a different thing from \"nobody's complained.\" It's a claim about the\ndistribution history, and it's the kind of claim you can actually check rather than\nassume. Dead code that runs a permission check first and does nothing is safe. Dead\ncode you're only fairly sure is dead is a liability wearing the same clothes.",[11,640,176],{"id":175},[16,642,643],{},"Rename the internal prefix in the same release as the visible name, not four\nreleases later. Splitting them meant the plugin spent a stretch of its life with a\npublic name that didn't match anything in its own codebase — which is confusing to\nread, and is exactly the kind of gap a second, unplanned rename can catch you\nin the middle of. The visible name is the one wordpress.org reviews. The internal\none is the one that actually has to be right, and it deserves to move on the same\nday.",{"title":187,"searchDepth":188,"depth":188,"links":645},[646,647,648,649,650],{"id":561,"depth":191,"text":562},{"id":574,"depth":191,"text":575},{"id":618,"depth":191,"text":619},{"id":631,"depth":191,"text":632},{"id":175,"depth":191,"text":176},{"src":652,"alt":653,"width":544,"height":545},"\u002Fimages\u002Fblog\u002Fwhat-renaming-a-plugin-actually-touches.webp","Abstract illustration for an article about renaming a WordPress plugin and migrating its internal identity","2026-09-03",{},"\u002Fblog\u002Fwhat-renaming-a-plugin-actually-touches",{"title":556,"description":187},"blog\u002Fwhat-renaming-a-plugin-actually-touches","A wordpress.org review rejected the name once. The rename itself was the easy part — the database table, the hooks API, and the REST namespace all had to move too, for a plugin that already had real installs.",[214,661,662],"php","products","yyGeKgB7V-MTe1mpjFtKFNbd162HcN-P5NvkBD-dNpA",{"id":665,"booking":666,"email":667,"extension":668,"meta":669,"name":670,"role":671,"socials":672,"stem":682,"summary":683,"whatsapp":684,"__hash__":685},"site\u002Fglobal\u002Fsite.json","https:\u002F\u002Fcal.com\u002Frupash\u002F30min","rupash.das.40@gmail.com","json",{},"Rupash Das","WordPress developer & systems engineer",[673,676,679],{"label":674,"url":675},"GitHub","https:\u002F\u002Fgithub.com\u002FRupashdas",{"label":677,"url":678},"WordPress.org","https:\u002F\u002Fprofiles.wordpress.org\u002Frupash\u002F",{"label":680,"url":681},"Fiverr","https:\u002F\u002Fwww.fiverr.com\u002Frupash_das","global\u002Fsite","I take a business problem from requirements to a WordPress system that runs it — architecture, custom plugins, integrations, performance and security, and the upkeep after launch. Eight years of client work, with Laravel and Vue when a CMS is the wrong tool.","https:\u002F\u002Fwa.me\u002F8801873139202","uy6RMn-wEZfo4zzWR3K9xre0PyN0-x0CfUFQ_Y_gVdE",{"id":687,"colophon":688,"ctaLabel":689,"extension":668,"meta":690,"stem":691,"tagline":692,"__hash__":693},"footer\u002Fglobal\u002Ffooter.json","Rupash Das · Built with Nuxt","Start a project",{},"global\u002Ffooter","WordPress systems — planned, built, connected and kept running. From Bangladesh, shipped worldwide.","U2ki7hPjtCWq-aVsL0Hf79eawJna8bsPQfdAUXvSyI0",[695,724,749,771,803,827],{"id":696,"category":697,"extension":668,"items":698,"meta":719,"order":720,"stem":721,"summary":722,"__hash__":723},"uses\u002Fpages\u002Fuses\u002F01-applications.json","Beyond WordPress",[699,703,707,711,715],{"name":700,"url":701,"note":702,"key":207},"Laravel","https:\u002F\u002Flaravel.com","The framework for anything that has outgrown a CMS. Prices and stock decided on the server, and feature tests that run against the real database rather than a mock of it.",{"name":704,"url":705,"note":706,"key":207},"Vue 3, Composition API","https:\u002F\u002Fvuejs.org","Composition over Options for anything non-trivial. Logic that belongs together lives together, and it can be pulled out into a composable the day a second screen needs it.",{"name":708,"url":709,"note":710},"Vite","https:\u002F\u002Fvite.dev","The build for everything — applications and plugin interfaces both. Fast enough that a React panel inside wp-admin stops feeling like a compromise.",{"name":712,"url":713,"note":714},"Tailwind","https:\u002F\u002Ftailwindcss.com","Tokens in one file, utilities everywhere else. The token layer is the part that matters; the utilities are only how it gets applied.",{"name":716,"url":717,"note":718},"Nuxt","https:\u002F\u002Fnuxt.com","This site. Prerendered, with a bundle budget that fails the build rather than being admired in a readme.",{},4,"pages\u002Fuses\u002F01-applications","What I reach for when a project outgrows a CMS and needs to be an application of its own.","AYQQ46_MRZtaMpLllg18x-vIKFwTOCQVKFrDv3xY7F4",{"id":725,"category":726,"extension":668,"items":727,"meta":745,"order":191,"stem":746,"summary":747,"__hash__":748},"uses\u002Fpages\u002Fuses\u002F02-local.json","Local development",[728,732,736,739,742],{"name":729,"url":730,"note":731,"key":207},"LocalWP","https:\u002F\u002Flocalwp.com","One throwaway site per plugin. A plugin has to be installed on a clean WordPress before I believe it works.",{"name":733,"url":734,"note":735,"key":207},"Laragon","https:\u002F\u002Flaragon.org","Where the Laravel side lives. Apache, PHP and MySQL without an afternoon of configuration.",{"name":737,"note":738},"MySQL","The database under both stacks. Enough of it that a data migration can be a query rather than a plugin.",{"name":740,"note":741,"key":207},"A test suite that runs locally","Just over a hundred suites on the form plugin alone. They exist because it ships to sites I will never see, and no amount of clicking replaces that.",{"name":743,"note":744},"A guarded release script","Building a release by hand is how a stray development file ends up in a zip. The script refuses rather than reminds me.",{},"pages\u002Fuses\u002F02-local","Two stacks, kept apart. WordPress work and application work do not share an environment.","jjnVioJgu2KQ94wsXA4f4vcf7TDjMf1p0nKZskI2C6k",{"id":750,"category":751,"extension":668,"items":752,"meta":767,"order":188,"stem":768,"summary":769,"__hash__":770},"uses\u002Fpages\u002Fuses\u002F03-terminal.json","Version control and terminal",[753,756,759,761,764],{"name":754,"note":755,"key":207},"Git","Every project is a repository, including the client sites that were never supposed to have one. It is also the reason I can work on a live site and still have a way back.",{"name":757,"note":758,"key":207},"Git Bash on Windows Terminal","POSIX habits on a Windows machine. Most tooling assumes a Unix shell, and it is less work to bring one than to translate every command.",{"name":674,"url":675,"note":760},"Public for the plugins, private for client work. Actions for anything that has to happen on a schedule.",{"name":762,"note":763},"Composer","PHP dependencies, and increasingly the autoloader inside plugins that used to get by without one.",{"name":765,"note":766},"npm","The other half of every build. Kept boring on purpose — a dependency in a plugin is a dependency in somebody's site.",{},"pages\u002Fuses\u002F03-terminal","The layer underneath everything else. Unglamorous, and the part I would miss first.","T-s-xcuuGET_9dm2M0uVTuhlZPPSeCAvRAiH65e1P4w",{"id":772,"category":773,"extension":668,"items":774,"meta":798,"order":799,"stem":800,"summary":801,"__hash__":802},"uses\u002Fpages\u002Fuses\u002F04-wordpress.json","The WordPress side",[775,779,783,786,790,794],{"name":776,"url":777,"note":778,"key":207},"React with @wordpress\u002Fcomponents","https:\u002F\u002Fdeveloper.wordpress.org\u002Fblock-editor\u002Freference-guides\u002Fcomponents\u002F","Plugin interfaces are applications mounted inside wp-admin. Using the WordPress components is why they look like they belong there rather than like something bolted on.",{"name":780,"url":781,"note":782,"key":207},"dnd-kit","https:\u002F\u002Fdndkit.com","Dragging that works from a keyboard and announces itself to a screen reader. Retrofitting that is far more expensive than choosing it.",{"name":784,"note":785,"key":207},"The WordPress REST API","The only seam between a plugin's PHP and its interface. It is what makes the PHP testable without a browser anywhere near it.",{"name":787,"url":788,"note":789},"Advanced Custom Fields Pro","https:\u002F\u002Fwww.advancedcustomfields.com","For client sites, where the content model lives. Fields named in the client's vocabulary, not the database's.",{"name":791,"url":792,"note":793},"Elementor","https:\u002F\u002Felementor.com","Not a compromise — the right answer when an in-house team edits weekly. Locked templates, a small palette, global styles set once.",{"name":795,"url":796,"note":797},"WooCommerce","https:\u002F\u002Fwoocommerce.com","With as few extension plugins as the requirement genuinely needs. Every add-on is a licence somebody has to keep paying.",{},1,"pages\u002Fuses\u002F04-wordpress","What goes into a plugin, and what goes onto a client site. Two different lists with two different standards.","j-NSUhkpvsb9v4-rYLa2FhTlS18t8gWF99Jgv4J-yDg",{"id":804,"category":805,"extension":668,"items":806,"meta":823,"order":209,"stem":824,"summary":825,"__hash__":826},"uses\u002Fpages\u002Fuses\u002F05-services.json","Hosting and delivery",[807,810,813,817,820],{"name":808,"note":809,"key":207},"The client's own hosting","Shared hosting, a control panel, no staging environment and no deploy pipeline. That is the honest description of most WordPress work, and pretending otherwise is how a rebuild gets quoted when a fix was needed.",{"name":811,"note":812,"key":207},"FTP against production","Not a preference, a constraint of agency work. It teaches one discipline above all: change one thing, verify it, keep a way back.",{"name":814,"url":815,"note":816,"key":207},"Cloudflare","https:\u002F\u002Fcloudflare.com","DNS, CDN and Pages. This site is prerendered and served from it.",{"name":818,"note":819},"A transactional email provider","Site mail leaves through a real provider, never the web host's PHP mail. This is the single most common reason a contact form fails silently.",{"name":821,"note":822},"Backups the client owns","To storage in their account, not mine. Leaving me should never mean leaving their data.",{},"pages\u002Fuses\u002F05-services","Mostly other people's infrastructure. The skill is working inside constraints I did not choose.","FbmQ8ReFvSj2JxfK-J7EKTve-3XXK3FlX2rLGYsSX4s",{"id":828,"category":829,"extension":668,"items":830,"meta":849,"order":447,"stem":850,"summary":851,"__hash__":852},"uses\u002Fpages\u002Fuses\u002F06-working.json","How I work",[831,834,837,840,843,846],{"name":832,"note":833,"key":207},"Model the data before the interface","The fastest way to make something expensive to own is to build the screens first and discover the shape of the data afterwards. True of a Laravel schema and of an ACF field group alike.",{"name":835,"note":836,"key":207},"Read the source before the documentation","Learned in two years of having nobody to ask. It is still the fastest route into an unfamiliar framework, and it is why picking up a new one is a week rather than a season.",{"name":838,"note":839,"key":207},"Ask who maintains it, before choosing how to build it","The same question opens every project. It decides page builder or custom theme, and it decides how much of an application belongs in its API rather than its front end.",{"name":841,"note":842},"Deep work in the morning, calls at night","US clients, Bangladesh time zone. The overlap is my evening, which leaves the mornings uninterrupted.",{"name":844,"note":845},"A repeated complaint is a specification","The same request from four unrelated clients is not an inconvenience. It is the only market research a one-person shop gets for free, and every plugin I have finished started there.",{"name":847,"note":848},"Write the handover in the maintainer's language","One of my plugins ships its maintainer notes in Bengali. Documentation somebody has to translate before using is documentation that will not be used.",{},"pages\u002Fuses\u002F06-working","Process rather than software. Most of it was learned the expensive way.","iaojSZmCa_ecT-IzSH3TzkN6K5Heshb2TELxA0DygII",[854],{"id":855,"title":856,"category":857,"extension":668,"image":858,"meta":864,"order":865,"stem":866,"__hash__":867},"gallery\u002Fpages\u002Fgallery\u002F06-plugin-settings.json","Settings, after the cuts","Plugins",{"src":859,"alt":860,"width":861,"height":862,"caption":863},"\u002Fimages\u002Fgallery\u002Fplugin-settings.webp","The spam settings screen in Defer Forms, with toggles for honeypot protection, a time-trap and duplicate-submission blocking",1400,1050,"Most of what a settings page wants to be belongs in the hooks API instead.",{},0,"pages\u002Fgallery\u002F06-plugin-settings","FnORlGVc8CiwZiDTKn47gc0jX559WJXPefZDoT_dpyA",{"id":869,"chapters":870,"extension":668,"intro":1046,"meta":1049,"stem":1050,"__hash__":1051},"journey\u002Fpages\u002Fjourney.json",[871,892,919,952,985,1020],{"id":872,"period":873,"role":874,"org":875,"place":876,"summary":877,"body":878,"highlights":881,"stack":884,"metrics":885,"image":886},"curiosity","2012 — 2016","Where the curiosity started","No computer of my own","Rural Bangladesh","A village where the electricity could not be relied on, and a kid who volunteered to charge other people's phones so he could spend an hour inside them.",[879,880],"Phones were charged off a solar panel. Neighbours would hand us one, I would plug it in and then spend the next hour opening every menu and changing every setting, and hours later we would find it almost exactly as flat as when it arrived. It is a funny story now. At the time it was the only access I had to a computer of any kind.","I got in front of a real one in 2012 and bought my own in 2016 — a low-powered dual-core desktop, paid for slowly. The reason it became a career rather than a hobby is unromantic: it was the one thing I owned that could plausibly change the next ten years.",[882,883],"Borrowed phones and a solar panel, in place of a computer","First real computer in 2012, first of my own in 2016",[],[],{"src":887,"alt":888,"width":889,"height":890,"caption":891},"\u002Fimages\u002Fjourney\u002Fcuriosity.webp","Diagram titled “Where the curiosity started”: five arched panels in a row, coloured yellow through purple, each with a small line icon and an arrow on to the next. In turn they read — volunteered to charge neighbours’ phones using a solar panel to gain access to the devices; spent an hour opening every menu and changing every setting on borrowed phones; got in front of a real computer for the first time in 2012; bought a low-powered dual-core desktop in 2016, paid for slowly; and transitioned from hobby to career because it could plausibly change the next ten years.",1200,588,"Five steps to a machine",{"id":893,"period":894,"role":895,"org":896,"place":897,"summary":898,"body":899,"highlights":903,"stack":907,"metrics":913,"image":914},"first-lines","2016 — 2018","Teaching myself to build","Self-taught, with one mentor","At home, on a dual-core desktop","Two years of front-end fundamentals learned with almost nothing available in my own language — which turned out to be the most useful constraint I have had.",[900,901,902],"An older brother pointed me at web development and told me where to start; that was most of the guidance available. Material in Bangla barely existed, my English was not strong enough to read the good English material quickly, and the community you can ask a question of today was a fraction of its current size. A problem a beginner would now solve in ten minutes could take me two or three days.","That is where the habit came from. When there is nobody to ask, you read the source, change one thing and watch what breaks — and I have used that on every unfamiliar codebase since, including every framework I have picked up as an adult.","By the end of it I could build and style a page properly, and I had enough PHP to stop treating the server as somebody else's problem. Which is what made the next step possible.",[904,905,906],"Front end first, then PHP — no framework in between","Learned to read source because there was nothing else to read","Two years of building for practice before charging anyone",[908,909,910,911,912],"HTML\u002FCSS","JavaScript","jQuery","Bootstrap","PHP",[],{"src":915,"alt":916,"width":889,"height":917,"caption":918},"\u002Fimages\u002Fjourney\u002Ffirst-lines.webp","Diagram titled “Self-Taught Developer’s Journey”: a figure climbing a bouldering wall, with four coloured holds labelled around it — Small Community, limited support for questions; Time Constraints, extended problem-solving duration; Language Barrier, restricted access to materials; and Weak English, slowed comprehension of resources.",1048,"What made it slow",{"id":920,"period":921,"role":922,"org":923,"place":924,"summary":925,"body":926,"highlights":930,"stack":933,"metrics":935,"image":942,"projects":947},"freelance","2018 — 2021","First paid work, then a specialism","Freelance, remote","Clients across five continents","First paid work in June 2018, and two years of turning design files into markup before WordPress arrived at the end of 2020.",[927,928,929],"The first paid job came in June 2018, two years after I started learning. For the next two years the work was almost entirely design-to-code conversion — a PSD or a Figma file in, hand-written HTML and CSS out. Narrow work, and a good place to get fast at the part everything else sits on.","The value was volume — a compressed run of briefs, deadlines and handovers that would have taken years to accumulate through referrals. Fixed prices meant every bad estimate came out of my own evenings, which is an expensive and very fast way to learn what to ask before quoting. That discipline is still the reason I can put a date on a piece of work and hold it.","WordPress arrived at the end of 2020. With HTML, CSS, JavaScript and PHP already in place it was not a new subject, it was a new arrangement of ones I knew — and it was where the work was. Within a few months it was most of what I sold, and a few months after that it was what an agency wanted a partner for.",[931,932],"Design-to-code conversion for the first two years, WordPress from late 2020","Clients across the US, Canada, Europe, Australia and New Zealand",[934,912,909,908,737],"WordPress",[936,939],{"value":937,"label":938},"223","orders delivered",{"value":940,"label":941},"100%","on-time",{"src":943,"alt":944,"width":889,"height":945,"caption":946},"\u002Fimages\u002Fjourney\u002Ffiverr.webp","Diagram titled “From Design to WordPress Specialism”: five figures walking left to right in the march-of-progress pose, from a knuckle-walking ape to an upright person, coloured yellow through purple. They are labelled Design-to-Code Conversion, turning design files into markup; Volume of Work, compressed briefs and deadlines; Learning from Mistakes, bad estimates cost personal time; WordPress Arrival, a new arrangement of known skills; and WordPress Specialist, agency partner for WordPress work.",751,"How the specialism arrived",[948,949,950,951],"kestopelti","applause","attribute-x","elder-care",{"id":953,"period":954,"role":955,"org":956,"place":957,"summary":958,"body":959,"highlights":963,"stack":967,"metrics":970,"image":977,"projects":982},"agency","2021 — 2026","White-label development partner","A US digital agency","Remote, US hours","Production sites across five sectors, built under someone else's name. The volume that turned preferences into a method — and produced the problems everything since is an answer to.",[960,961,962],"White-label means the work ships under the agency's name: their designer's file, their project manager, their client. Feedback arrives specific and immediate, measured against a file rather than an opinion, which is uncomfortable for about a month and then extremely useful.","The range is wider than a portfolio usually admits — marketing sites, custom themes with modelled content, page builders where the client's own team had to keep editing, LMS and education sites, WooCommerce stores, and a great deal of ordinary CMS work, often on hosting nobody involved chose.","That much volume is enough to stop guessing. The decision that mattered on each one turned out not to be technical: it was who would be maintaining this in a year. The other thing it produced was a list — the same handover questions, the same requests, the same awkward edit, repeating across unrelated clients. Written down, that stops being anecdotes and becomes a specification.",[964,965,966],"Sites across construction, industrial, hospitality, education and e-commerce","Custom themes, page builders, WooCommerce and LMS — chosen by who maintains the site","Working US hours from Bangladesh, under an agency's review process",[934,968,969,791,795,737],"PHP 8","ACF Pro",[971,974],{"value":972,"label":973},"45","sites built",{"value":975,"label":976},"5","years with one partner",{"src":978,"alt":979,"width":889,"height":980,"caption":981},"\u002Fimages\u002Fjourney\u002Fagency.webp","Timeline titled “Key Milestones in White-Label Development Partnership”, running left to right through five points: Partnership Established, 5 years; Production Volume, 45 sites; Maintenance Decision, 1 year; Feedback Adaptation, 1 month; and Specification Development, ongoing.",556,"The partnership, in milestones",[983,984],"sorbet-magazine","miss-suomi",{"id":986,"period":987,"role":988,"org":989,"place":990,"summary":991,"body":992,"highlights":996,"stack":1000,"metrics":1005,"image":1012,"projects":1017},"plugins","2024 — Present","WordPress plugins of my own","Independent","Patenga, Chattogram, working remotely","Taking that list seriously, and shipping it to WordPress.org. Plugins built as real applications inside wp-admin — React over a PHP core, talking through REST — rather than settings pages with a form on them.",[993,994,995],"Nice Select for WP went into the directory first. Defer Forms for Contact Form 7 came straight off the list after it: a visual builder for Contact Form 7, around 12,400 lines of PHP behind a React canvas, built so that no form already living on a site has to change. Both follow the same rule — take a job clients kept asking me to repeat, and make it something they can do themselves.","Building them is where the modern front-end work actually started. A drag-and-drop canvas with selection, undo and a properties panel is not a jQuery problem; it is a React application that happens to be mounted inside somebody else's admin. The PHP side stopped knowing anything about the interface and started answering REST requests instead, which is the same seam I would later build an entire API across.","The discipline that came with them was the WordPress.org review process — coding standards, escaping, internationalisation, nothing bundled that should not be. Useful to have applied to your own code rather than only to a client's.",[997,998,999],"Two plugins public on WordPress.org","React interfaces over a PHP core, joined by REST rather than by templates","Extension points documented as a hooks API before any settings page",[1001,1002,780,1003,1004,708],"PHP 8.2","React","@wordpress\u002Fcomponents","WordPress REST API",[1006,1009],{"value":1007,"label":1008},"~12k","lines of PHP in Defer Forms for Contact Form 7",{"value":1010,"label":1011},"112","test suites",{"src":1013,"alt":1014,"width":889,"height":1015,"caption":1016},"\u002Fimages\u002Fjourney\u002Fplugins.webp","A suspension bridge drawn across a valley, its deck labelled “Build React plugins with REST API”. The near bank reads “Clients repeat requests, requiring manual intervention”; the far bank reads “Clients can build and manage plugins independently”.",809,"From requests to self-service",[1018,1019],"defer-forms-for-contact-form-7","nice-select-for-wp",{"id":1021,"period":1022,"role":1023,"org":989,"place":990,"summary":1024,"body":1025,"highlights":1030,"stack":1034,"metrics":1037,"image":1038,"projects":1043},"applications","Now","WordPress, and beyond it","WordPress is still most of the work. Laravel and Vue are for the projects that outgrow it — built where none of the answers a CMS hands you arrive for free.",[1026,1027,1028,1029],"WordPress answers most of the hard application questions before you start — users, roles, storage, routing — and it answers them one particular way. The plugins kept running into the edge of that. A permission rule about a record rather than a type of thing is not a capability; a table you actually want to query is not post meta. At some point the honest move is to build where those decisions are yours.","So alongside WordPress there is now Laravel: JSON APIs, prices and stock decided on the server rather than trusted from the browser, migrations, and feature tests that run against the real database. Shopfront POS is the worked example — including the audit that found my own code selling stock it did not have. On the front end, Vue 3 and Nuxt, which is what this site is built in.","The patterns that keep proving themselves are the ones the WordPress years taught me. Model the data before the interface. Keep the dependency count low, because every one is something somebody inherits. And decide by who maintains it, which turns out to be as true of an API contract as it ever was of a client's homepage.","WordPress client work and my own plugins remain the bulk of it — the applications make me better at both.",[1031,1032,1033],"WordPress sites and plugins, still the core of the work","Laravel applications with JSON APIs and PHPUnit feature tests","Vue 3 and Nuxt, including this site",[700,737,1035,1036,716,708,712],"PHPUnit","Vue 3",[],{"src":1039,"alt":1040,"width":889,"height":1041,"caption":1042},"\u002Fimages\u002Fjourney\u002Fapplications.webp","Diagram titled “Custom Full-Stack Architecture”: two boxes joined by a handshake. Backend Logic lists a robust PHP framework foundation, a secure API authentication layer and structured relational data storage; Frontend Interface lists a reactive single-page application framework, centralised domain state management and a utility-first CSS styling system.",761,"Two halves, one contract",[1044,1045],"arvale","inventory-pos",[1047,1048],"Six stages, and each one is the reason the next was possible. Learning alone taught me to read source; freelancing taught me to scope; the agency taught me WordPress at volume and to a standard; the volume produced the problems my plugins answer; and building those plugins is what took me into React, and then into Laravel and Vue for the work a CMS should not be asked to carry.","This is the long version. The short one is on the About page.",{},"pages\u002Fjourney","LbWOmgP9glhftv0G3TwzPNTk4peya5cvxeedhDRpXEk",[1053,1061,1070,1076,1083,1091,1097,1103,1110,1116,1122,1128,1135,1142,1149,1157,1163,1169,1175,1182],{"path":1054,"title":1055,"kicker":1056,"cover":1057},"\u002Fprojects\u002Fapplause","Applause","Client project · Frontend build",{"src":1058,"alt":1059,"width":544,"height":1060},"\u002Fimages\u002Fprojects\u002Fapplause-cover.webp","The Applause homepage hero, showing a hand-held picture frame over water beneath the headline about the future of native plant species",1000,{"path":1062,"title":1063,"kicker":1064,"cover":1065},"\u002Fprojects\u002Farvale","Arvale","Own build · Elementor Pro & WooCommerce store",{"src":1066,"alt":1067,"width":1068,"height":1069},"\u002Fimages\u002Fprojects\u002Farvale-cover.webp","A laptop and phone showing the Arvale homepage, the headline \"Clothes, bags and objects made to be kept\" beside a fashion photo above a large typographic category index, next to the Arvale logo and the labels Responsive Design, Custom Elementor Build and WooCommerce",1672,941,{"path":1071,"title":1072,"kicker":1056,"cover":1073},"\u002Fprojects\u002Fattribute-x","AttributeX Solutions",{"src":1074,"alt":1075,"width":544,"height":1060},"\u002Fimages\u002Fprojects\u002Fattribute-x-cover.webp","The AttributeX Solutions site navigation and a news section headlined \"Tech-Savvy!\" beside an isometric illustration of a person working at a laptop",{"path":1077,"title":1078,"kicker":1079,"cover":1080},"\u002Fprojects\u002Fdefer-forms-for-contact-form-7","Defer Forms for Contact Form 7","WordPress plugin",{"src":1081,"alt":1082,"width":544,"height":1060},"\u002Fimages\u002Fprojects\u002Fdefer-forms-cover.webp","The Defer Forms for Contact Form 7 product banner: the plugin name beside a mockup of the builder, with a field palette on the left, a text field being dragged onto the form canvas, and panels for notifications and stored submissions alongside",{"path":1084,"title":1085,"kicker":1056,"cover":1086},"\u002Fprojects\u002Felder-care","Elder Care",{"src":1087,"alt":1088,"width":1089,"height":1090},"\u002Fimages\u002Fprojects\u002Fec-website-cover.webp","The Elder Care homepage hero, \"Parents living away?\", beside an older couple having coffee, above a row of elder-care service cards",1448,1086,{"path":1092,"title":1093,"kicker":1056,"cover":1094},"\u002Fprojects\u002Felite-competitions","Elite Competitions",{"src":1095,"alt":1096,"width":1089,"height":1090},"\u002Fimages\u002Fprojects\u002Felite-cover.webp","The Elite Competitions homepage hero, \"We Give Amazing Prizes\", showing a car giveaway slide above a \"How It Works\" section",{"path":1098,"title":1099,"kicker":1056,"cover":1100},"\u002Fprojects\u002Feu-skladi","EU-Skladi",{"src":1101,"alt":1102,"width":1089,"height":1090},"\u002Fimages\u002Fprojects\u002Feu-skladi-cover.webp","The EU-Skladi homepage hero, \"Na strani E-skladov\", with a Programs, Projects, Razpisi and Novice card row beneath it",{"path":1104,"title":1105,"kicker":1106,"cover":1107},"\u002Fprojects\u002Fforma","Forma","Concept build · WordPress, Elementor and a custom plugin",{"src":1108,"alt":1109,"width":200,"height":442},"\u002Fimages\u002Fprojects\u002Fforma-cover.webp","The Forma identity board: the wordmark and the line \"Architecture & Interior Design\" beside four screens of the site — the \"Buildings, first as models.\" hero with a 3D study model, a dark Selected Work carousel, a six-discipline services list and a project page for House in Nature",{"path":1111,"title":1112,"kicker":1056,"cover":1113},"\u002Fprojects\u002Ffreelance-engine","FreelanceEngine",{"src":1114,"alt":1115,"width":1089,"height":1090},"\u002Fimages\u002Fprojects\u002Ffreelance-engine-cover.webp","The FreelanceEngine homepage split hero, \"The best way to find a professional\" and \"Need a job? Tell us your story\", above a project listings table",{"path":1117,"title":1118,"kicker":1056,"cover":1119},"\u002Fprojects\u002Ffurni","FURNI",{"src":1120,"alt":1121,"width":1089,"height":1090},"\u002Fimages\u002Fprojects\u002Ffurni-cover.webp","The FURNI homepage hero, \"Modern Furniture for a Better Home\", above a popular categories grid and featured products row",{"path":1123,"title":1124,"kicker":1056,"cover":1125},"\u002Fprojects\u002Fholders","Holders",{"src":1126,"alt":1127,"width":1089,"height":1090},"\u002Fimages\u002Fprojects\u002Fholders-cover.webp","The Holders homepage hero, \"Shopping in USA now made easy\", showing a package-forwarding illustration and shipping calculator, product marketplace and account tiles",{"path":1129,"title":1130,"kicker":1131,"cover":1132},"\u002Fprojects\u002Finventory-pos","Shopfront POS","Point of sale and stock control",{"src":1133,"alt":1134,"width":544,"height":1060},"\u002Fimages\u002Fprojects\u002Finventory-pos-cover.svg","The words \"Shopfront POS\" set on pale slate, beside four shelf columns standing in for stock levels, the third nearly empty and lit in indigo",{"path":1136,"title":1137,"kicker":1138,"cover":1139},"\u002Fprojects\u002Fkestopelti","Kestopelti","Fiverr client · Frontend build",{"src":1140,"alt":1141,"width":544,"height":1060},"\u002Fimages\u002Fprojects\u002Fkestopelti-cover.webp","The Kestopelti homepage, showing a full-bleed roofing photograph behind the headline \"Peltikatteet ja kattovarusteet\" and the site's top navigation",{"path":1143,"title":1144,"kicker":1056,"cover":1145},"\u002Fprojects\u002Fkids-zone","Kids Zone",{"src":1146,"alt":1147,"width":544,"height":1148},"\u002Fimages\u002Fprojects\u002Fkidszone-cover.webp","The Kids Zone homepage, showing \"Popular\" and \"Latest\" game category tabs above a grid of game thumbnails",764,{"path":1150,"title":1151,"kicker":1152,"cover":1153},"\u002Fprojects\u002Fmiss-suomi","Miss Suomi Beauty Academy","Client project · WordPress & Elementor build",{"src":1154,"alt":1155,"width":1068,"height":1156},"\u002Fimages\u002Fprojects\u002Fmiss-suomi-cover.webp","A laptop and phone showing the Miss Suomi Beauty Academy homepage, a winter portrait behind a booking prompt and a row of pageant event cards, beside the Miss Suomi crest logo and a \"Built with WordPress + Elementor\" badge",940,{"path":1158,"title":1159,"kicker":1056,"cover":1160},"\u002Fprojects\u002Fnero-whyte","Nero Whyte",{"src":1161,"alt":1162,"width":1089,"height":1090},"\u002Fimages\u002Fprojects\u002Fnero-whyte-cover.webp","The Nero Whyte site on a laptop and tablet, showing the black-and-white \"We Are Transparent\" page beside the brand's zigzag logo",{"path":1164,"title":1165,"kicker":1079,"cover":1166},"\u002Fprojects\u002Fnice-select-for-wp","Nice Select for WP",{"src":1167,"alt":1168,"width":544,"height":1060},"\u002Fimages\u002Fprojects\u002Fnice-select-for-wp-cover.webp","The Nice Select for WP promotional graphic: a default browser select box open on a fruit list, beside the plugin's searchable multi-select showing three chosen fruits as removable tags above a search field and checkmarked list, under the headline \"Beautiful, flexible and easy-to-use select dropdowns for WordPress.\"",{"path":1170,"title":1171,"kicker":1056,"cover":1172},"\u002Fprojects\u002Fpricesearch","Pricesearch",{"src":1173,"alt":1174,"width":1089,"height":1090},"\u002Fimages\u002Fprojects\u002Fpricesearch-cover.webp","The Pricesearch homepage hero, \"Easily monitor your competitors' prices\", beside a competitor price-tracking dashboard mockup",{"path":1176,"title":1177,"kicker":1178,"cover":1179},"\u002Fprojects\u002Fsorbet-magazine","Sorbet Magazine","Client project · Elementor & WooCommerce build",{"src":1180,"alt":1181,"width":200,"height":442},"\u002Fimages\u002Fprojects\u002Fsorbet-cover.webp","A laptop and phone showing the Sorbet Magazine homepage, a \"The Fashion Issue\" hero above a row of latest articles, beside the Sorbet logo and the note \"built with Elementor + WooCommerce\"",{"path":1183,"title":1184,"kicker":1056,"cover":1185},"\u002Fprojects\u002Fspire-global","Spire Global",{"src":1186,"alt":1187,"width":1089,"height":1090},"\u002Fimages\u002Fprojects\u002Fspire-global-cover.webp","The Spire Global homepage hero, \"Sustainability. A greener tomorrow.\", above a mission statement and forest photograph",1794132401237]