Why the old block setup slowed teams down
A custom WordPress block used to mean writing the same block twice. Once in PHP, once in JavaScript. That split made sense on paper. The server registered the block, the editor bundle taught Gutenberg how to display it, and everyone pretended the mirror image would stay perfectly in sync forever. It usually didn’t.
In the older workflow, the PHP side handled block registration and the frontend output. The JavaScript side handled the editor experience. So if a block had a name, icon, category, attributes, support flags, or editor script handle, those details often lived in two places. One file might say the block accepts an alignment option. The other file might forget that option entirely. One file might point to the new editor script. The other might still reference the old one. That’s how a simple block starts collecting weird little mismatches.
When the same block gets defined twice, the odds of a mismatch go up fast. Code has a long memory for that kind of thing.
For a solo developer, that duplication is annoying. For a small team, it can turn into a steady trickle of maintenance work. Every new block means extra setup. Every edit means checking both definitions. Every refactor means hunting through two files, sometimes more, to make sure the same change happened in both places. Nobody wakes up excited to update the same label in PHP and JavaScript before coffee.
The real headache shows up when the two versions drift apart. Maybe the editor shows one title, but the frontend uses another. Maybe the block supports a feature in the inserter, but the server-side registration never picked it up. Maybe a teammate tweaks the JavaScript version, ships it, and the PHP file quietly keeps doing the old thing. Then you get bug reports that are hard to reproduce because the block behaves one way in the editor and another way on the page. Fun for no one.
That’s why this old setup slowed teams down more than it first appeared to. The issue wasn’t just duplicate code. It was duplicate responsibility. Every extra file gave the block another chance to drift.
Once you’ve maintained a few custom blocks this way, the appeal of a leaner setup gets obvious fast. Less repetition means fewer places for mistakes to hide, less time spent checking the same settings twice, and a smoother path from idea to shipped block. The next step is the part that changes the workflow: letting PHP take the lead so WordPress can do more of the wiring for you.

PHP as the source of truth
Once you stop treating the PHP file and the JavaScript file like equal twins, the setup gets a lot easier to reason about. The block definition lives on the server first. That means the block name, metadata, attributes, scripts, styles, and supports can all be defined in one place, instead of copied into a second registration path just so the editor can recognize the block too.
For a lot of WordPress custom blocks, that single place is a block.json file paired with PHP registration. The block.json format gives WordPress a structured description of the block, and the block registration docs show how PHP can register it without you rebuilding the same setup in JavaScript by hand. WordPress reads that server-side definition and fills in the editor-facing wiring from there.
One block definition in PHP is easier to trust than two versions that have to stay in sync by hand.
That single-source approach cuts out a lot of repeated configuration. You’re no longer writing the same block slug, category, asset handles, and attribute schema in two different files and hoping nobody forgets to update one of them after a later tweak. If the block title changes, or an asset reference moves, or an attribute gets renamed, there’s one definition to adjust. That’s the part teams feel first. Less copy-paste. Fewer tiny mismatches. Fewer moments where the editor says one thing and the rendered block says another.
It also changes the shape of the work. Instead of starting from a JavaScript registration file and then mirroring it in PHP, you register the block on the server and let WordPress build the editor-side setup from that registration. The server file becomes the source of truth, which is a better fit for a lot of projects because the backend already knows the block name, metadata, and asset locations. You’re not asking two files to independently describe the same thing. You’re asking one file to describe it once.
That said, this is not a crusade against JavaScript. It still has a job. If your block needs live editor controls, custom previews, interactive panels, or frontend behavior that depends on client-side code, JavaScript stays in the picture. The point here is narrower and more practical: don’t duplicate what WordPress can already infer. Keep the definition in PHP, let WordPress read the metadata, and reserve JavaScript for the parts that actually need it. For teams maintaining a growing set of blocks, that trade pays off quickly, because the block definition stops living in two places and starts living where it belongs.
What WordPress handles for you
Once the block starts life on the PHP side, WordPress can take care of a surprising amount of the editor wiring for you. If you define the block with register_block_type() and point it at the block metadata, WordPress reads the setup, loads the right assets, and figures out how the block should appear in the editor without you writing a separate manual JavaScript registration file. The register_block_type() reference spells out the core function, while the block file structure guide shows how the pieces usually fit together.
That means the editor can still treat the block like a normal Gutenberg block. Its name, attributes, supports, scripts, styles, and other metadata can be pulled from the registration details instead of copied into a second file by hand. You don’t have to repeat the same block name in PHP and JavaScript, then hope both versions still agree after the next round of edits. WordPress can read the metadata and connect the dots for you, which is a pleasant change from the old “write it twice, debug it once” routine.
The less you duplicate the block definition, the less room there is for the editor to go wandering off on its own.
In practical terms, this means your editor assets can be attached where they belong without extra plumbing. If the block needs an editor script, WordPress knows where to load it. If it needs editor styles, those can be registered alongside the block instead of bolted on later. If the block has declared attributes or support settings, the editor can use them without a separate registration layer trying to restate the same facts. The result is a block that behaves correctly in the editor while the registration work stays in one place.
That setup is especially handy when you want WordPress to manage the boring bits. Not the creative bits, obviously. You still decide what the block does, how it renders, and whether it needs custom JavaScript for interaction. But the routine plumbing, the asset loading, the metadata, the boilerplate that tends to multiply across a project, can be handled by WordPress itself. For maintainers, that usually means fewer files to open, fewer sync problems to chase, and fewer “wait, which copy is the real one?” moments at 4:55 p.m.
It also makes the editor feel less fragile. When the server-side registration is the source of truth, the block can still show up properly in the inserter, expose the right settings in the sidebar, and load the scripts it needs in the right context. You’re not gambling on a hand-written JavaScript registration file staying perfectly matched to the PHP version forever. WordPress does the translation work, and the block keeps behaving the same way in the editor as long as the metadata stays accurate.
That leaves you with a cleaner setup and a smaller surface area for mistakes, which is the kind of trade most teams are happy to make before the codebase gets noisy. Next, we can look at the simplest practical pattern for setting this up without turning the project into a tiny maintenance hobby.
A practical registration pattern
Once you stop duplicating block settings in two places, the setup gets pleasantly boring. That’s a good thing. The usual pattern is simple: define the block metadata once, register it from PHP, and let WordPress read the rest from that metadata file.
In practice, that often means a block.json file sitting next to your block code, plus one small PHP call to register it. WordPress can read the same metadata for the block name, title, description, category, editor script, style handles, and whatever else you’ve put in there. If you want the shortest path possible, the PHP side can be just one line, which is about as close as block registration gets to “no nonsense.”
Here’s the shape of it:
<?php
/**
* Plugin Name: My Custom Blocks
*/
add_action( 'init', function () {
register_block_type_from_metadata( __DIR__ . '/build/card' );
} );
That function is documented in the WordPress reference for PLINK_16, and it does exactly what the name suggests. You point WordPress at a block directory, and it reads the metadata from there instead of making you restate everything in a separate registration file.
A minimal block folder might look like this:

my-blocks/
└── build/
└── card/
├── block.json
├── index.asset.php
└── index.js
The part that matters most for the registration pattern is block.json. That file becomes the single place where the block’s identity and asset references live. The PHP file stays tiny, and the WordPress developer workflow gets a lot less fussy because you are not maintaining a second mirror copy of the same settings.
A compact block.json might read like this:
{
"apiVersion": 2,
"name": "acme/card",
"title": "Card",
"category": "widgets",
"icon": "index-card",
"editorScript": "file:./index.js",
"style": "file:./style.css"
}
That’s enough to keep the setup readable without turning the project into a scavenger hunt. When a teammate opens the block folder later, they can see the whole registration story in one place. No guessing which file wins. No comparing two nearly identical definitions and wondering why one says acme/card while the other says acme-card. That sort of drift is exactly where small bugs like to hide.
One source of truth is less glamorous than two synchronized files, but it’s a lot easier to keep alive.
This is where the pattern pays off in custom block development, especially when you build a lot of blocks for one site or client. A team can copy the same folder structure, change the metadata, and keep moving. The setup stays consistent across blocks, which makes reviews easier and reduces the little “wait, why does this one work differently?” moments that eat time in code review.
The maintenance side is where things get especially tidy. If a block title changes, or the category moves, or you rename an asset, you update the metadata once. You’re not hunting through separate PHP and JavaScript registrations to make sure both files say the same thing. Fewer duplicated settings means fewer chances to miss a spot, and fewer spots to miss means fewer surprises when the block is deployed.
For a small team, that can feel like the difference between a tidy toolbox and a drawer full of duplicate screwdrivers. Both technically work. One of them is easier to live with.
When to use this approach—and when not to
If a custom block mostly needs a name, an icon, a few settings, and predictable output, this server-first registration pattern is a very good fit. Think about blocks that behave the same way every time they appear in the editor: a simple callout, a testimonial card, a pricing note, a team bio, maybe a lightweight content wrapper with a handful of attributes. For those, the extra JavaScript registration file can feel like paperwork. You define the block once, let WordPress read the metadata, and move on with your day instead of babysitting two copies of the same setup.
If a block works fine with server-side registration and a light editor wrapper, there’s no prize for adding JavaScript drama.
That said, not every block wants to live in this streamlined lane. Once a block starts depending on richer editor behavior, the tradeoff shifts. Maybe the inspector sidebar changes state based on several inputs. Maybe the block needs live previews, drag-and-drop interactions, or client-side calculations that affect the editor UI as the user types. Maybe the front end needs more than a static render and has to respond to clicks, filters, tabs, or other interaction without a page reload. In those cases, dedicated JavaScript still earns its place. The point here is not to ban client-side code. It’s to avoid writing it when the block doesn’t ask for it.
For teams maintaining older blocks, the safest move is to simplify one step at a time. A block that has been split across PHP and JavaScript for years may depend on small bits of duplicated setup that are easy to miss during cleanup. After you switch to a metadata-driven registration flow, test the editor and the front end separately. Open the block in the editor, check that the controls still appear where they should, and confirm that saved content renders the same way on the site. Then test the front-end output in a few real templates, because theme styles and block wrappers can make a harmless-looking change behave differently in practice. The WordPress block metadata reference is worth keeping nearby during that cleanup, since it shows which settings belong in the metadata file and which pieces still need code.
There’s also a judgment call around team workflow. If a small agency or solo builder needs to ship ten custom blocks quickly, the simpler model usually wins. Fewer files to open. Fewer places for a label, attribute, or script handle to drift out of sync. Fewer “why is the editor doing one thing and the front end doing another?” moments, which tend to show up right before a deadline, naturally. On the other hand, if your project revolves around a complex editing experience, a more explicit JavaScript setup can be easier to reason about because all the behavior lives in one place. That’s a fair trade when the block really needs it.
So the rule of thumb is fairly plain: use the streamlined registration path when consistency, speed, and low maintenance matter more than custom client-side behavior. Reach for fuller JavaScript when the block’s value depends on richer interaction inside the editor or on the page itself. Most teams sit somewhere between those two poles, which is why this approach is so handy. It gives you a simpler default, and then lets you add complexity only where the block actually asks for it.
The payoff: fewer files, fewer bugs, faster shipping
Once you’ve trimmed the registration flow down to one source of truth, the messy little costs start disappearing. You stop copying block settings into a second file just to keep WordPress happy. You stop checking whether the PHP version and the JavaScript version still match after a late-night edit. You stop doing the tiny but annoying ritual of “fix one file, then remember to fix the other.”
That sounds small until you’ve maintained a handful of custom blocks for a client site, then another site, then a third one with its own odd requirements. The duplication adds up fast. One block needs its title changed. Another gets a new attribute. A third gets a tweak to its editor script. If those definitions live in two places, drift sneaks in. The editor might show one thing while the frontend does another. Or a block works locally and breaks after a deployment because one side got updated and the other side did not.
The less you mirror, the less you have to babysit.
For agencies and small teams, that matters in a very ordinary, very practical way. It means fewer review comments that boil down to “why does this file still say the old value?” It means less time spent tracing a bug back to a duplicated block definition. It also means a cleaner handoff for the next developer who opens the project and has to figure out how the block was registered in the first place. One registration path is easier to scan, easier to explain, and easier to keep stable over time.
The quality-of-life gain shows up in the day-to-day rhythm of shipping. A block update becomes one edit instead of two. A new custom block follows the same pattern as the last one. The codebase gets a little less brittle each time you avoid a mirrored definition, and that tends to pay off in boring but welcome ways: fewer mismatches, fewer regressions, fewer “why is this behaving differently here?” moments.
So the takeaway is pretty plain. If WordPress can handle the client-side wiring from a single PHP registration step, let it. One definition is easier to maintain than two separate ones, and in block work, that usually means less friction, fewer bugs, and a faster path from idea to shipped page.





