[{"data":1,"prerenderedAt":919},["ShallowReactive",2],{"gate-notes":3,"layout-site":134,"layout-footer":152,"gate-uses":160,"gate-gallery":324,"project-\u002Fprojects\u002Fkestopelti":369,"projects-siblings":438},[4],{"id":5,"title":6,"body":7,"cover":114,"date":119,"description":105,"draft":120,"extension":121,"meta":122,"navigation":123,"path":124,"placeholder":120,"readingMinutes":125,"seo":126,"stem":127,"summary":128,"tags":129,"__hash__":133},"blog\u002Fblog\u002Fwhat-renaming-a-plugin-actually-touches.md","What renaming a plugin actually touches",{"type":8,"value":9,"toc":104},"minimark",[10,15,19,22,25,29,37,71,74,78,81,84,87,91,94,97,101],[11,12,14],"h2",{"id":13},"the-second-rename-was-not-my-idea","The second rename was not my idea",[16,17,18],"p",{},"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,20,21],{},"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,23,24],{},"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,26,28],{"id":27},"a-name-is-not-just-a-label","A name is not just a label",[16,30,31,32,36],{},"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 ",[33,34,35],"em",{},"encoded"," took four\nmore releases to find and fix, one at a time:",[38,39,40,48,59,65],"ul",{},[41,42,43,47],"li",{},[44,45,46],"strong",{},"The database table."," Submissions, roles, everything the plugin stores lived\nunder a table prefixed with the old internal short name.",[41,49,50,53,54,58],{},[44,51,52],{},"The developer hooks."," Seven of them, documented, with real signatures — anyone\nwho had already filtered on ",[55,56,57],"code",{},"old_prefix_submission_created"," needed to know it was\nnow something else, not silently stop firing.",[41,60,61,64],{},[44,62,63],{},"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.",[41,66,67,70],{},[44,68,69],{},"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,72,73],{},"None of this shows up in a diff of the plugin's header comment. All of it is the\nactual rename.",[11,75,77],{"id":76},"the-migration-has-to-run-once-and-only-once","The migration has to run once, and only once",[16,79,80],{},"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,82,83],{},"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,85,86],{},"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,88,90],{"id":89},"knowing-when-its-safe-to-delete","Knowing when it's safe to delete",[16,92,93],{},"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,95,96],{},"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,98,100],{"id":99},"what-id-do-differently","What I'd do differently",[16,102,103],{},"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":105,"searchDepth":106,"depth":106,"links":107},"",3,[108,110,111,112,113],{"id":13,"depth":109,"text":14},2,{"id":27,"depth":109,"text":28},{"id":76,"depth":109,"text":77},{"id":89,"depth":109,"text":90},{"id":99,"depth":109,"text":100},{"src":115,"alt":116,"width":117,"height":118},"\u002Fimages\u002Fblog\u002Fwhat-renaming-a-plugin-actually-touches.webp","Abstract illustration for an article about renaming a WordPress plugin and migrating its internal identity",1600,900,"2026-09-03",false,"md",{},true,"\u002Fblog\u002Fwhat-renaming-a-plugin-actually-touches",6,{"title":6,"description":105},"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.",[130,131,132],"wordpress","php","products","yyGeKgB7V-MTe1mpjFtKFNbd162HcN-P5NvkBD-dNpA",{"id":135,"email":136,"extension":137,"meta":138,"name":139,"role":140,"socials":141,"stem":148,"summary":149,"whatsapp":150,"__hash__":151},"site\u002Fglobal\u002Fsite.json","rupash.das.202@gmail.com","json",{},"Rupash Das","Full-stack developer",[142,145],{"label":143,"url":144},"GitHub","https:\u002F\u002Fgithub.com\u002FRupashdas",{"label":146,"url":147},"Fiverr","https:\u002F\u002Fwww.fiverr.com\u002Frupash_das","global\u002Fsite","Laravel and Vue applications, and React-based WordPress plugins. Behind them: eight years of client work, and around 45 production sites shipped as a US agency's development partner.","https:\u002F\u002Fwa.me\u002F8801873139202","EeGW4F_WwVqwPebxGBi6Bks5X-7JlYlrabI1_3un-1M",{"id":153,"colophon":154,"ctaLabel":155,"extension":137,"meta":156,"stem":157,"tagline":158,"__hash__":159},"footer\u002Fglobal\u002Ffooter.json","Rupash Das · Built with Nuxt","Start a project",{},"global\u002Ffooter","Applications, APIs and plugins. Built in Bangladesh, shipped worldwide.","ioAbs2Z6IXBmsOAL3B48yjlepJt_zX4shLK2YbnqVHc",[161,194,219,241,273,298],{"id":162,"category":163,"extension":137,"items":164,"meta":189,"order":190,"stem":191,"summary":192,"__hash__":193},"uses\u002Fpages\u002Fuses\u002F01-applications.json","The application stack",[165,169,173,177,181,185],{"name":166,"url":167,"note":168,"key":123},"Laravel","https:\u002F\u002Flaravel.com","An API that could have a second client put in front of it, rather than a framework quietly rendering its own front end. Sanctum for authentication; the authorisation layer is written by hand.",{"name":170,"url":171,"note":172,"key":123},"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":174,"url":175,"note":176,"key":123},"Pinia","https:\u002F\u002Fpinia.vuejs.org","One store per domain, each owning its own loading and error state. A single global store is comfortable for a week and unreadable by the third feature.",{"name":178,"url":179,"note":180},"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":182,"url":183,"note":184},"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":186,"url":187,"note":188},"Nuxt","https:\u002F\u002Fnuxt.com","This site. Prerendered, with a bundle budget that fails the build rather than being admired in a readme.",{},1,"pages\u002Fuses\u002F01-applications","What I reach for when the thing being built is an application rather than a page.","hGL4lHq-kkVaIrg0fv7gwqYxrk8AVgOVVNf80UJ2_tU",{"id":195,"category":196,"extension":137,"items":197,"meta":215,"order":109,"stem":216,"summary":217,"__hash__":218},"uses\u002Fpages\u002Fuses\u002F02-local.json","Local development",[198,202,206,209,212],{"name":199,"url":200,"note":201,"key":123},"Laragon","https:\u002F\u002Flaragon.org","Where the Laravel side lives. Apache, PHP and MySQL without an afternoon of configuration.",{"name":203,"url":204,"note":205,"key":123},"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":207,"note":208},"MySQL","The database under both stacks. Enough of it that a data migration can be a query rather than a plugin.",{"name":210,"note":211,"key":123},"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":213,"note":214},"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. Application work and WordPress work do not share an environment.","O6x-4PI-YpMIY4lMnasic5o5S74m0XvaSJobTsv9zo0",{"id":220,"category":221,"extension":137,"items":222,"meta":237,"order":106,"stem":238,"summary":239,"__hash__":240},"uses\u002Fpages\u002Fuses\u002F03-terminal.json","Version control and terminal",[223,226,229,231,234],{"name":224,"note":225,"key":123},"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":227,"note":228,"key":123},"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":143,"url":144,"note":230},"Public for the plugins, private for client work. Actions for anything that has to happen on a schedule.",{"name":232,"note":233},"Composer","PHP dependencies, and increasingly the autoloader inside plugins that used to get by without one.",{"name":235,"note":236},"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":242,"category":243,"extension":137,"items":244,"meta":268,"order":269,"stem":270,"summary":271,"__hash__":272},"uses\u002Fpages\u002Fuses\u002F04-wordpress.json","The WordPress side",[245,249,253,256,260,264],{"name":246,"url":247,"note":248,"key":123},"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":250,"url":251,"note":252,"key":123},"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":254,"note":255,"key":123},"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":257,"url":258,"note":259},"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":261,"url":262,"note":263},"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":265,"url":266,"note":267},"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.",{},4,"pages\u002Fuses\u002F04-wordpress","What goes into a plugin, and what goes onto a client site. Two different lists with two different standards.","9KoxuzjlxZ4muWCbNdHrgY5KXOxLFueIx9g9orrTomo",{"id":274,"category":275,"extension":137,"items":276,"meta":293,"order":294,"stem":295,"summary":296,"__hash__":297},"uses\u002Fpages\u002Fuses\u002F05-services.json","Hosting and delivery",[277,280,283,287,290],{"name":278,"note":279,"key":123},"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":281,"note":282,"key":123},"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":284,"url":285,"note":286,"key":123},"Cloudflare","https:\u002F\u002Fcloudflare.com","DNS, CDN and Pages. This site is prerendered and served from it.",{"name":288,"note":289},"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":291,"note":292},"Backups the client owns","To storage in their account, not mine. Leaving me should never mean leaving their data.",{},5,"pages\u002Fuses\u002F05-services","Mostly other people's infrastructure. The skill is working inside constraints I did not choose.","FbmQ8ReFvSj2JxfK-J7EKTve-3XXK3FlX2rLGYsSX4s",{"id":299,"category":300,"extension":137,"items":301,"meta":320,"order":125,"stem":321,"summary":322,"__hash__":323},"uses\u002Fpages\u002Fuses\u002F06-working.json","How I work",[302,305,308,311,314,317],{"name":303,"note":304,"key":123},"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":306,"note":307,"key":123},"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":309,"note":310,"key":123},"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":312,"note":313},"Deep work in the morning, calls at night","US clients, Dhaka time zone. The overlap is my evening, which leaves the mornings uninterrupted.",{"name":315,"note":316},"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":318,"note":319},"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.","TXfbDM6GA9PGTV6J6tU1Hgl8j10rEHfQnjVVFF0cYe0",[325,338,348,358],{"id":326,"title":327,"category":328,"extension":137,"image":329,"meta":335,"order":190,"stem":336,"__hash__":337},"gallery\u002Fpages\u002Fgallery\u002F01-tazko-board.json","Kanban board","Applications",{"src":330,"alt":331,"width":332,"height":333,"caption":334},"\u002Fimages\u002Fgallery\u002Ftazko-board.webp","A kanban board in Tazko with Todo, In Progress, Review and Done columns and task cards carrying priority and due date",1400,1050,"Dragging updates the store first and reconciles against the API. The server still owns the order.",{},"pages\u002Fgallery\u002F01-tazko-board","TgaWh_sQJBBgJ6hawBbHU4gnRWEJFjCwfDP171Cc1ao",{"id":339,"title":340,"category":328,"extension":137,"image":341,"meta":345,"order":109,"stem":346,"__hash__":347},"gallery\u002Fpages\u002Fgallery\u002F02-permissions-matrix.json","The permission matrix",{"src":342,"alt":343,"width":332,"height":333,"caption":344},"\u002Fimages\u002Fgallery\u002Fpermissions-matrix.webp","The Tazko role editor showing capability groups for Activity, Clients and Comments, with 73 of 80 permissions enabled for the Admin role","Roles composed from capabilities, so a workspace can define its own without a release.",{},"pages\u002Fgallery\u002F02-permissions-matrix","PL75HpzIO1ckSp-VJiV57O07DaiqgK2p5ZAFrD6OjvA",{"id":349,"title":350,"category":328,"extension":137,"image":351,"meta":355,"order":106,"stem":356,"__hash__":357},"gallery\u002Fpages\u002Fgallery\u002F03-tazko-task.json","A task discussion",{"src":352,"alt":353,"width":332,"height":333,"caption":354},"\u002Fimages\u002Fgallery\u002Ftazko-task.webp","A Tazko task detail view with a subtask checklist at 67 percent and a three-message discussion thread below it","One polymorphic thread serves tasks and projects. The reply you have not sent is saved on the server, not in the tab.",{},"pages\u002Fgallery\u002F03-tazko-task","my5OA-xN5tuWban5-uTAOfVRIVLQZ8Mt-ZJ71uIfLZg",{"id":359,"title":360,"category":361,"extension":137,"image":362,"meta":366,"order":125,"stem":367,"__hash__":368},"gallery\u002Fpages\u002Fgallery\u002F06-plugin-settings.json","Settings, after the cuts","Plugins",{"src":363,"alt":364,"width":332,"height":333,"caption":365},"\u002Fimages\u002Fgallery\u002Fplugin-settings.webp","The spam settings screen in Defer Forms, with toggles for honeypot protection, a time-trap and duplicate-submission blocking","Most of what a settings page wants to be belongs in the hooks API instead.",{},"pages\u002Fgallery\u002F06-plugin-settings","11L8JKOQF2SaNPqMAzxj49pHozh58LqUmBF0Zm3N9QY",{"id":370,"title":371,"body":372,"cover":400,"description":105,"eta":404,"extension":121,"facts":405,"featured":120,"kicker":417,"liveUrl":418,"meta":419,"metrics":420,"navigation":123,"order":421,"path":422,"placeholder":120,"repoUrl":404,"role":423,"sector":424,"seo":425,"shots":426,"stack":427,"status":432,"stem":433,"summary":434,"type":435,"year":436,"__hash__":437},"projects\u002Fprojects\u002Fkestopelti.md","Kestopelti",{"type":8,"value":373,"toc":395},[374,378,381,385,388,392],[11,375,377],{"id":376},"context","Context",[16,379,380],{},"Kestopelti is a Finnish family-run manufacturer of steel roofing sheets, roof\naccessories and rainwater systems, shipping from factories in Joensuu and\nKuopio. This came in as a Fiverr order: a design already existed, and the job\nwas to turn it into a working front end.",[11,382,384],{"id":383},"what-i-built","What I built",[16,386,387],{},"The design arrived as a finished file, not a brief — I wasn't the designer.\nMy part was the HTML, CSS and JavaScript: turning that design into a\nresponsive layout with Bootstrap, matched to the source file across\nbreakpoints, and handed off ready to be wired into whatever the client's team\nran on the backend. What that backend became, and who has touched the site\nsince, isn't something I can speak to — my scope stopped at the front end I\ndelivered.",[11,389,391],{"id":390},"why-its-here","Why it's here",[16,393,394],{},"Most of what's on this page is bigger — an application, a plugin, years with\none agency. This is a single, smaller Fiverr order, and it earns its place\nfor a plainer reason: it's real, paid client work, delivered to spec, and the\nsite it became is still live today.",{"title":105,"searchDepth":106,"depth":106,"links":396},[397,398,399],{"id":376,"depth":109,"text":377},{"id":383,"depth":109,"text":384},{"id":390,"depth":109,"text":391},{"src":401,"alt":402,"width":117,"height":403},"\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",1000,null,[406,409,412,414],{"label":407,"value":408},"Role","Frontend developer",{"label":410,"value":411},"Client","Finnish roofing manufacturer",{"label":413,"value":146},"Delivered via",{"label":415,"value":416},"Scope","Frontend only","Fiverr client · Frontend build","https:\u002F\u002Fkestopelti.fi\u002F",{},[],11,"\u002Fprojects\u002Fkestopelti","Frontend developer — HTML\u002FCSS\u002FJS build from a supplied design","Roofing & building materials",{"title":371,"description":105},[],[428,429,430,431],"HTML5","CSS3","JavaScript","Bootstrap","live","projects\u002Fkestopelti","The responsive front end for a Finnish steel-roofing manufacturer's site — coded from a supplied design and still live today.","case-study","Delivered","CAlRPDYcpM5BebcEyJCxdgMwKEGidds8X_4DOgk1cJk",[439,589,772,888],{"id":440,"title":441,"body":442,"cover":546,"description":105,"eta":549,"extension":121,"facts":550,"featured":123,"kicker":561,"liveUrl":404,"meta":562,"metrics":563,"navigation":123,"order":190,"path":564,"placeholder":120,"repoUrl":404,"role":565,"sector":566,"seo":567,"shots":568,"stack":578,"status":584,"stem":585,"summary":586,"type":587,"year":549,"__hash__":588},"projects\u002Fprojects\u002Ftazko.md","Tazko",{"type":8,"value":443,"toc":537},[444,451,455,458,461,465,468,471,475,478,481,485,489,495,501,507,511,514,517,521,524,527,531,534],[445,446,448],"callout",{"type":447},"note",[16,449,450],{},"In build, and written up as such. Everything below describes what is in the\ncodebase today — there are no results yet, because it has no users yet. The\nscreenshots are the real application running locally, on seeded data.",[11,452,454],{"id":453},"why-i-am-building-it","Why I am building it",[16,456,457],{},"Five years of WordPress teaches you a great deal about content and comparatively\nlittle about application state. WordPress answers most of the hard questions for\nyou — users, roles, storage, routing — and it answers them one particular way.",[16,459,460],{},"I wanted a project where none of those answers arrived for free, at a size where\nthe decisions would be real rather than tutorial-shaped. Task and project\nmanagement is a good choice for that, because it looks simple from the outside\nand is almost entirely permissions on the inside.",[11,462,464],{"id":463},"what-it-has-to-get-right","What it has to get right",[16,466,467],{},"Who can see this project. Who can move that task. Who can be invited, and by\nwhom. What a stranger holding a link is allowed to open. What happens to a\ncomment when the thing it was attached to is deleted.",[16,469,470],{},"Get that layer wrong and every feature above it is unsafe, so it is the layer\neverything else waits for.",[11,472,474],{"id":473},"architecture","Architecture",[16,476,477],{},"Two applications and one contract: a Laravel 12 REST API authenticated with\nSanctum, and a Vue 3 single-page application that knows nothing about Laravel\nbeyond the shape of its responses.",[16,479,480],{},"Deliberately not Inertia. The point of the exercise was an API I could put a\nsecond client in front of — and a front end that has to deal with loading,\nfailure and reconciliation honestly, rather than being handed server-rendered\nstate.",[482,483],"stat-row",{":items":484},"[{\"value\":\"14\",\"label\":\"models\"},{\"value\":\"28\",\"label\":\"migrations\"},{\"value\":\"75\",\"label\":\"API endpoints\"}]",[11,486,488],{"id":487},"three-decisions-worth-explaining","Three decisions worth explaining",[16,490,491,494],{},[44,492,493],{},"Roles composed from capabilities."," Off-the-shelf permission packages model a\nrole as a label with a list attached. I needed the inverse: the capability as\nthe unit, roles built out of them, every check asking about a capability rather\nthan a role name. Eighty capabilities across fourteen modules live in a single\nregistry the seeder reads, so adding a new kind of member later is a data change\ninstead of a code change.",[16,496,497,500],{},[44,498,499],{},"Share links as their own concept."," A share link is not a user with fewer\nrights. It is a separate grant against a single attachment, with its own\n48-character token, its own expiry, its own on\u002Foff switch, its own\ndownload-or-view-only flag, and its own access count. Modelling it as a weak\nuser is the mistake that leaks data.",[16,502,503,506],{},[44,504,505],{},"Ten Pinia stores, not one."," Each owns a domain and its own loading and error\nstate. A single global store is comfortable for about a week and unreadable by\nthe third feature.",[11,508,510],{"id":509},"what-exists-today","What exists today",[16,512,513],{},"Seventy-four single-file components across boards, task detail, comments,\ninvitations and settings. TipTap for rich text, a draggable kanban that updates\nthe store optimistically and reconciles against the API's answer, polymorphic\ncomments with likes, subtasks with their own ordering, project members and\nlabels, email invitations with an accept flow, and tokened share links for\nattachments.",[16,515,516],{},"The part I did not plan and now would not remove is drafts. An unfinished\ncomment is saved against a context key on the server, not in the browser, so it\nsurvives a closed tab and follows you to another machine.",[11,518,520],{"id":519},"what-is-honestly-not-finished","What is honestly not finished",[16,522,523],{},"Time tracking and the in-app messaging screen are built as front-end\nprototypes over fixture data — the components are real, the API behind them\nis not written yet. They are in the application and they are not in this case\nstudy's screenshots, because a screenshot of a working timer would be a claim\nI cannot support.",[16,525,526],{},"That is the current shape of the project: the permission layer, the data model\nand the collaboration features are wired end to end; the two features I\nprototyped ahead of the backend are waiting for it.",[11,528,530],{"id":529},"what-is-left","What is left",[16,532,533],{},"Those two backends, deployment, and the first users breaking it in ways I have\nnot thought of. When that happens this page gets a URL and an outcome section it\nhas not earned yet.",[16,535,536],{},"What the project has already produced is the thing I wanted from it: Laravel,\nVue, Pinia and REST are no longer frameworks I have read about. They are\nframeworks I have made decisions in and had to live with.",{"title":105,"searchDepth":106,"depth":106,"links":538},[539,540,541,542,543,544,545],{"id":453,"depth":109,"text":454},{"id":463,"depth":109,"text":464},{"id":473,"depth":109,"text":474},{"id":487,"depth":109,"text":488},{"id":509,"depth":109,"text":510},{"id":519,"depth":109,"text":520},{"id":529,"depth":109,"text":530},{"src":547,"alt":548,"width":117,"height":403},"\u002Fimages\u002Fprojects\u002Ftazko-cover.svg","The word \"Tazko\" set on slate ground, beside three uneven columns standing in for a kanban board, the middle one lit in indigo","2026",[551,553,555,558],{"label":407,"value":552},"Solo developer",{"label":474,"value":554},"Laravel API + Vue SPA",{"label":556,"value":557},"API","14 models, 75 endpoints",{"label":559,"value":560},"Permissions","80 capabilities, 14 modules","Project management app",{},[],"\u002Fprojects\u002Ftazko","Solo — data model, API, permissions, front end","Web applications",{"title":441,"description":105},[569,574],{"src":570,"alt":571,"width":117,"height":572,"caption":573},"\u002Fimages\u002Fprojects\u002Ftazko-01.webp","A Tazko task detail screen showing subtasks at 67 percent, a three-message discussion thread, assignees, status and priority",853,"Task detail. Comments are polymorphic — the same thread code serves tasks and projects — and an unsent one is saved server-side, so closing the tab does not lose it.",{"src":575,"alt":576,"width":117,"height":572,"caption":577},"\u002Fimages\u002Fprojects\u002Ftazko-02.webp","The Tazko role editor, showing the Admin role with 73 of 80 permissions enabled, grouped into modules","Roles are composed from capabilities rather than hard-coded. Admin holds 73 of the 80; the seven it does not hold are the point.",[579,580,581,174,582,583,207,178],"Laravel 12","Sanctum","Vue 3","Vue Router","Tailwind 4","building","projects\u002Ftazko","A task and project management application — a Laravel API and a separate Vue single-page front end, with a permission system that had to be designed rather than installed.","application","Y8h_kddT3-3WAlT5jJEM-3aFSyX47y6orOrp4upG87s",{"id":590,"title":591,"body":592,"cover":723,"description":105,"eta":404,"extension":121,"facts":726,"featured":123,"kicker":737,"liveUrl":404,"meta":738,"metrics":739,"navigation":123,"order":106,"path":749,"placeholder":120,"repoUrl":750,"role":751,"sector":752,"seo":753,"shots":754,"stack":763,"status":432,"stem":768,"summary":769,"type":770,"year":549,"__hash__":771},"projects\u002Fprojects\u002Fdefer-forms-for-contact-form-7.md","Defer Forms for Contact Form 7",{"type":8,"value":593,"toc":713},[594,596,599,602,606,609,612,616,619,622,626,635,641,647,653,657,660,663,683,687,690,693,697,700,703,706,710],[11,595,377],{"id":376},[16,597,598],{},"Contact Form 7 is one of the most installed plugins in WordPress, and it is\nconfigured by typing bracket tags into a textarea. That is fine for a developer\nbuilding one form. It is not fine for the person who owns the site afterwards,\nand that is the person who needs to change the form six months later.",[16,600,601],{},"I know the shape of this problem from the other end. Every handover I have done\nended the same way: a document explaining how to edit the form, then an email\nthree weeks later asking me to edit the form.",[11,603,605],{"id":604},"the-problem","The problem",[16,607,608],{},"Rewriting the plugin was never an option. Contact Form 7 sits under an enormous\nnumber of live sites, including many I did not build, and any tool that asks an\nowner to migrate their forms has already failed — the forms are wired into mail\ntemplates, spam filtering, third-party integrations and, on some sites, years of\nhabit.",[16,610,611],{},"So the constraint was set before the first line: a form that exists today must\nkeep working exactly as it does, untouched, whether or not anyone ever opens the\nnew interface.",[11,613,615],{"id":614},"approach","Approach",[16,617,618],{},"The plugin is a layer, not a replacement. Contact Form 7 remains the thing that\nrenders, validates and mails the form. Defer Forms owns the\nediting experience and compiles what you build into the tag syntax CF7 already\nunderstands, so the generated output is something the plugin — and any developer\nreading it — treats as ordinary.",[16,620,621],{},"That decision paid for itself repeatedly. Anything CF7 can do, an escape hatch\ncan still do; anything CF7 cannot do stays out of scope instead of turning into\na fork I would have to maintain against every upstream release.",[11,623,625],{"id":624},"technical-decisions","Technical decisions",[16,627,628,631,632,634],{},[44,629,630],{},"A React canvas, mounted inside the WordPress admin."," The builder is a real\napplication — drag and drop, selection, undo, a properties panel — and pretending\notherwise with jQuery would have cost more than it saved. ",[55,633,250],{}," handles the\ndragging, because it is keyboard-operable and accessible by default rather than\nby later effort.",[16,636,637,640],{},[44,638,639],{},"The REST API as the seam."," The builder talks to the plugin over REST routes\nand nothing else. The PHP side has no idea a React application exists, which is\nwhat makes the PHP side testable on its own.",[16,642,643,646],{},[44,644,645],{},"Submissions as a database table, not post meta."," Form entries are stored as\nrecords, with the field mapping kept alongside them, so a submission stays\nreadable after the form it came from has been edited.",[16,648,649,652],{},[44,650,651],{},"A hooks API before a settings page."," Seven documented hooks sit on a\nsubmission — keep a form out of the table, change what is stored before it is\nwritten, act on an entry once it exists — and they are the plugin's public\nsurface. They are documented in the readme the plugin ships with rather than in\na markdown file beside it, because a reference belongs where somebody looking\nfor it would actually go: the plugin's own page.",[11,654,656],{"id":655},"implementation","Implementation",[16,658,659],{},"Seventeen features ship in the free plugin. The ones that changed the shape of\nthe codebase most:",[482,661],{":items":662},"[{\"value\":\"~12,400\",\"label\":\"lines of PHP\"},{\"value\":\"22\",\"label\":\"JS and JSX source files\"},{\"value\":\"112\",\"label\":\"test suites, all passing\"}]",[38,664,665,668,671,674,677,680],{},[41,666,667],{},"A submissions database with export, so a client can read what people sent\nwithout opening their inbox.",[41,669,670],{},"Multi-step forms, which required the compiler to think about state that\nContact Form 7 has no concept of.",[41,672,673],{},"Conditional logic, edited as rules and compiled to markup and behaviour.",[41,675,676],{},"Revisions, because a form is content and content gets broken by whoever edited\nit last.",[41,678,679],{},"GDPR export and erase, wired into WordPress's own personal-data tools rather\nthan a private screen of my own.",[41,681,682],{},"Notifications to Telegram, Slack and Discord for the clients who do not read\nemail.",[11,684,686],{"id":685},"challenges","Challenges",[16,688,689],{},"The hardest part was not the builder. It was compilation fidelity: taking a\nvisual structure and producing tag syntax that CF7 parses into precisely the\nform the user drew, including the cases where the visual model is richer than\nthe syntax. Where the two could not be reconciled, the builder refuses to\nproduce something it cannot round-trip rather than quietly producing something\nclose.",[16,691,692],{},"The second was scope. A form builder attracts feature requests indefinitely, and\nthe answer to most of them is the hooks API rather than another checkbox in a\nsettings page.",[11,694,696],{"id":695},"outcome","Outcome",[16,698,699],{},"The plugin is released at v2.6.7 and public on GitHub, with a documented hooks\nAPI, a guarded release script, and a maintainer document written in Bengali so\nthe person maintaining it in my own language can do so without translating my\ncode comments first.",[16,701,702],{},"It is in review for the WordPress plugin directory, and that review is where\nthe current name came from: the previous one led with a generic word and sat\ntoo close to an existing plugin's. Renaming a plugin is mostly unglamorous\nwork — an internal prefix, a database table, a REST namespace, every admin\naddress — and it is worth recording that the awkward part of shipping is\nrarely the part that was interesting to build.",[16,704,705],{},"What is genuinely finished is the thing that mattered at the start: existing\nforms do not have to change.",[11,707,709],{"id":708},"what-i-would-take-to-the-next-one","What I would take to the next one",[16,711,712],{},"The escape hatch is why this works. Every tool that sits on top of an\nestablished plugin is really a bet that you can improve the interface without\ninheriting the maintenance of everything underneath it — and the bet only pays\nif the user can always drop back down to the layer below.",{"title":105,"searchDepth":106,"depth":106,"links":714},[715,716,717,718,719,720,721,722],{"id":376,"depth":109,"text":377},{"id":604,"depth":109,"text":605},{"id":614,"depth":109,"text":615},{"id":624,"depth":109,"text":625},{"id":655,"depth":109,"text":656},{"id":685,"depth":109,"text":686},{"id":695,"depth":109,"text":696},{"id":708,"depth":109,"text":709},{"src":724,"alt":725,"width":117,"height":403},"\u002Fimages\u002Fprojects\u002Fdefer-forms-cover.svg","The words \"Defer Forms for Contact Form 7\" set on warm paper, beside four short bars standing in for form fields, one lit in burnt-orange",[727,728,731,734],{"label":407,"value":552},{"label":729,"value":730},"Release","v2.6.7, public on GitHub",{"label":732,"value":733},"Codebase","~12,400 lines of PHP",{"label":735,"value":736},"Builder","React + dnd-kit","WordPress plugin",{},[740,743,746],{"value":741,"label":742},"0","existing forms that have to change",{"value":744,"label":745},"17","features shipped in the free plugin",{"value":747,"label":748},"112","test suites, all passing","\u002Fprojects\u002Fdefer-forms-for-contact-form-7","https:\u002F\u002Fgithub.com\u002FRupashdas\u002Fdefer-forms-for-contact-form-7","Solo — architecture, plugin, builder UI, tests, release","WordPress products",{"title":591,"description":105},[755,759],{"src":756,"alt":757,"width":117,"height":403,"caption":758},"\u002Fimages\u002Fprojects\u002Fdefer-forms-01.webp","A field's settings panel open in the builder, showing the label, field name and placeholder for an email field","The canvas. Every field is a block, and the shortcode is generated underneath rather than typed.",{"src":760,"alt":761,"width":117,"height":403,"caption":762},"\u002Fimages\u002Fprojects\u002Fdefer-forms-02.webp","The conditional logic editor, showing an AND rule built from two field conditions with a value comparison","Conditional rules are edited as rules and compiled down to Contact Form 7's own tag syntax.",[764,765,766,250,767,178,207],"WordPress","PHP 8.2","React","WordPress REST API","projects\u002Fdefer-forms-for-contact-form-7","A drag-and-drop builder for Contact Form 7 — canvas, conditional logic and a submissions database — built as a layer that changes nothing about the forms already on a site.","plugin","qEIvoU1y5kOWDZUYyxmEu4i20RcR5yWNoU77gtaMzBE",{"id":773,"title":774,"body":775,"cover":849,"description":105,"eta":404,"extension":121,"facts":852,"featured":120,"kicker":865,"liveUrl":404,"meta":866,"metrics":867,"navigation":123,"order":868,"path":869,"placeholder":120,"repoUrl":404,"role":870,"sector":871,"seo":872,"shots":873,"stack":882,"status":432,"stem":885,"summary":886,"type":435,"year":549,"__hash__":887},"projects\u002Fprojects\u002Fdevrupash-com.md","devrupash.com",{"type":8,"value":776,"toc":843},[777,781,784,787,791,797,803,809,823,827,830,833,837,840],[11,778,780],{"id":779},"why-it-exists-in-this-stack","Why it exists in this stack",[16,782,783],{},"I sell WordPress work, so the obvious move was to build this in WordPress. I\nbuilt it in Nuxt instead, for one reason: the modern-stack half of what I do\nneeded something public that could be inspected rather than described.",[16,785,786],{},"It is a small site with a lot of engineering underneath it, which is roughly the\nargument.",[11,788,790],{"id":789},"the-decisions","The decisions",[16,792,793,796],{},[44,794,795],{},"Prerendered, not server-rendered."," Every page is HTML on a CDN before anyone\nasks for it. One server route survives, for the contact form, because that is\nthe only thing on the site that genuinely needs a server.",[16,798,799,802],{},[44,800,801],{},"A budget that fails the build."," The JavaScript budget is 130KB gzipped and it\nis checked as part of the production build, not admired in a readme. The\nhomepage currently ships about 104KB.",[16,804,805,808],{},[44,806,807],{},"Content as typed data, not as pages."," Every project, post, capability and\ntimeline entry is validated against a schema at build time — required alt text,\nrequired image dimensions, no capability listed under two headings. A file that\ndoes not validate stops the build rather than rendering an empty section.",[16,810,811,814,815,818,819,822],{},[44,812,813],{},"An accessibility guard in the same place."," Heading order, one ",[55,816,817],{},"h1"," and one\n",[55,820,821],{},"main"," per page, alt text, image dimensions, labels. It is cheaper to enforce\nthan to audit.",[11,824,826],{"id":825},"constraints-i-set-myself","Constraints I set myself",[16,828,829],{},"No animation library, no component library, no analytics with a consent banner\nattached. Two typefaces. One accent colour. Every interaction on the site is one\nof six named CSS moves, each with a keyboard-focus equivalent, and none of them\nanimate a layout property.",[16,831,832],{},"The sound effects are synthesised in about forty lines of Web Audio rather than\ndownloaded, so the site ships no audio files at all.",[11,834,836],{"id":835},"what-it-is-not","What it is not",[16,838,839],{},"It is not a demonstration that Nuxt is better than WordPress. Half the sites I\nbuild would be worse in this stack, because their owners need to edit them on a\nTuesday afternoon without calling anybody.",[16,841,842],{},"It is a demonstration that I can hold a build to a standard when there is no\nclient asking me to.",{"title":105,"searchDepth":106,"depth":106,"links":844},[845,846,847,848],{"id":779,"depth":109,"text":780},{"id":789,"depth":109,"text":790},{"id":825,"depth":109,"text":826},{"id":835,"depth":109,"text":836},{"src":850,"alt":851,"width":117,"height":403},"\u002Fimages\u002Fprojects\u002Fdevrupash-com-cover.webp","The homepage of devrupash.com, showing the intro section with headline, portrait and the stat row",[853,856,859,862],{"label":854,"value":855},"Stack","Nuxt 4, prerendered",{"label":857,"value":858},"JS budget","130KB, enforced",{"label":860,"value":861},"Tests","362, in CI",{"label":863,"value":864},"Hosting","Cloudflare Pages","This site",{},[],10,"\u002Fprojects\u002Fdevrupash-com","Solo — design system, build, content model","Personal",{"title":774,"description":105},[874,878],{"src":875,"alt":876,"width":117,"height":403,"caption":877},"\u002Fimages\u002Fprojects\u002Fdevrupash-com-01.webp","The projects page, filtered to All, showing the four real case studies with their category counts","Filtered by type — application, plugin, client work — counted from the content files themselves, so a filter can never disagree with what's actually published.",{"src":879,"alt":880,"width":117,"height":403,"caption":881},"\u002Fimages\u002Fprojects\u002Fdevrupash-com-02.webp","A blog post page showing the title, summary, tags and the start of the article body","A post's heading structure comes from Nuxt Content's own parse, not a DOM scrape — so the \"On this page\" list beside it can never drift from the article.",[883,581,583,884,864],"Nuxt 4","TypeScript","projects\u002Fdevrupash-com","The site you are reading — prerendered Nuxt 4 on Cloudflare, built with a bundle budget, an accessibility guard and a content schema that refuses to publish invented copy.","mNRCqcUPOk3cPepLz2XvGRC7jg12PMaAGm9vCXDcSMw",{"id":370,"title":371,"body":889,"cover":908,"description":105,"eta":404,"extension":121,"facts":909,"featured":120,"kicker":417,"liveUrl":418,"meta":914,"metrics":915,"navigation":123,"order":421,"path":422,"placeholder":120,"repoUrl":404,"role":423,"sector":424,"seo":916,"shots":917,"stack":918,"status":432,"stem":433,"summary":434,"type":435,"year":436,"__hash__":437},{"type":8,"value":890,"toc":903},[891,893,895,897,899,901],[11,892,377],{"id":376},[16,894,380],{},[11,896,384],{"id":383},[16,898,387],{},[11,900,391],{"id":390},[16,902,394],{},{"title":105,"searchDepth":106,"depth":106,"links":904},[905,906,907],{"id":376,"depth":109,"text":377},{"id":383,"depth":109,"text":384},{"id":390,"depth":109,"text":391},{"src":401,"alt":402,"width":117,"height":403},[910,911,912,913],{"label":407,"value":408},{"label":410,"value":411},{"label":413,"value":146},{"label":415,"value":416},{},[],{"title":371,"description":105},[],[428,429,430,431],1788434573323]