Blog

Stop Rebuilding Every WordPress Site

A practical, objection-by-objection guide to standardizing WordPress builds with theme.json and block patterns—without making every client site cookie-cutter.

Summary

Most agencies build every WordPress site from a blank theme, even when a shared foundation would cut weeks out of the schedule. This article argues that theme.json, block patterns, and dynamic blocks let you standardize the structural layer while preserving each client's distinctive design. It directly addresses the five objections that keep teams from changing: ‘we have different clients’, ‘custom blocks are expensive’, ‘the editor is confusing’, ‘we'll lose our hooks and filters’, and ‘FSE isn’t production-ready’. Each objection gets a practical counter-argument and a concrete pattern you can adopt incrementally. The payoff is a repeatable build process that still honors bespoke work where it belongs. Warning: no one-click reset buttons are promised.

How many of your client sites share even one line of code? Not the copyright line—actual code. If the answer is "barely any," you've already felt the pain: the same hero section rebuilt for the ninth time, the same team-grid markup copied from one project to the next, the same preprocess tweaks cross-referenced across half a dozen themes. You've also heard the defense: "Every client has different needs." True. The conclusion everyone draws, though—that every site needs a bespoke foundation—is false. The WordPress ecosystem now gives you a way to standardize the structural pieces without standardizing the design: theme.json for design tokens, block patterns for repeated layout, and dynamic blocks for the handful of features that need real server-side logic. This article is about the objections that keep agencies from taking that step, and what actually works when you push back against them.

The "but every client is different" objection

The underlying principle: standardize the foundation, not the surface. The reason to keep structure in a shared library is precisely to leave the visual layer free. A theme.json file is not a design—it's a set of design tokens. Colors, spacing, and typography are values, not markup. That's the critical shift: you can share the markup while a per-site theme.json makes the site look completely different for a different brand.

Take two clients: a law firm and an outdoor retailer. Their design languages are miles apart. But both need a hero section, a testimonial grid, a call-to-action band. Instead of rebuilding the markup for each, maintain three block patterns and let each client's theme.json define colors, fonts, and spacing. The structure stays identical; the design tokens turn it from one brand into the other. When the retailer changes its color palette next spring, you edit one file on their site—not the markup in six templates.

Practically, this means your team creates patterns as code, registers them in a shared plugin, and lets theme.json on each client site handle the paint. The pattern's class names become your architecture; the values become the variables. You can even go further and extend theme.json to include custom settings for post types or plugin output, though at some point you're building a configuration interface instead of a site—a trap discussed in our look at extending theme.json. Keep the shared layer lean: it should contain only what recurs across clients. The moment you find yourself adding a setting "in case someone someday wants it," you've created an abstraction that will cost more to maintain than it saves.

When you set up a new client, the first thirty minutes should be: clone the shared patterns plugin, create a new theme.json with the client's palette and font scale, and register their logo and footer. That's not a custom build; it's a configuration task. The remaining client-specific work goes into content, structure, and any genuinely bespoke features. This is the difference between building every house from scratch and having a set of prefabricated floor plans that you can repaint and re-wallpaper. The analogy is loose, but the principle holds: the more you push into theme.json values, the less you have to touch markup.

One of the simplest wins is to actually look at how block patterns work. A pattern is just a collection of blocks with predefined content and styling. You can save any block configuration as a pattern, and then a client can insert it without needing to know how it's built. That means the pattern becomes an 'entry point' for non-technical users. When your team maintains the underlying pattern in code, the client gets a consistent library without touching a single PHP tag.

Now, the caveat I keep coming back to: don't overcentralize. A theme.json with a setting for every conceivable nuance is a maintenance swamp. Shared patterns should be opinionated, not omnipotent. If a client needs a radically different layout—say, a magazine homepage with a big featured grid—they might not fit your standard pattern library. That's okay. Standardization means you win on the 80% of projects that are similar, not that you force every site into the same mold.

The "custom blocks blow the budget" objection

Here's a counter-principle that sounds boring but saves money: most things you think need a custom block don't. Core blocks plus a pattern can cover the overwhelming majority of layouts. The custom block is the last resort, not the first intention.

The classic example is the team grid. If it's a one-off, use the core "columns" and "group" blocks and let the client drop in an avatar by hand. If three clients ask for the same grid with the same "social links below the name" structure, you now have a candidate for a block pattern. When that pattern starts collecting new options—hover effects, sorting, rating stars—the pattern becomes an unmanageable grab bag, and then it's time to write a custom block. The budget-hurting mistake is jumping straight to the custom block on first request.

A more insidious scenario: the client asks for a "case study carousel." The first instinct is to think, "I need a carousel block." But do they need a carousel? Maybe they need a horizontally scrollable group of posts, which core blocks can handle with a "group" block and some CSS. Or maybe they need a dynamic list of recent case studies, which is a dynamic block that queries the CPT. The question is not "what feature does the client want?" but "what data does it depend on?" If the data is static and editable by the client, a pattern will do. If the data comes from a database query, a dynamic block is justified. If the data needs to update in real-time from an API, you might be looking at a REST API integration instead—that crosses into a different kind of build.

When you do build a block, block.json is your friend. It's the single source of truth for attributes, scripts, and styles, which makes the block portable across projects. It also lets you declare dependencies and translations cleanly, which is essential when you distribute a library across many client sites. For content that depends on live data, a dynamic block renders on the server, so you don't need to ship a JavaScript bundle on every page view. And if your block evolves, you can handle deprecations gracefully so existing content doesn't break—our guide to block deprecation walks through the exact pattern.

Before you build anything, run the decision through this grid:

ApproachBest forAvoid when
Core blockOne-off content, simple pagesThe layout is repeated across many clients and needs rich options
Block patternRepeatable layouts without logicThe layout needs conditionals, dynamic data, or complex interactions
Custom blockRepeated, data-driven, or highly specific behaviorThe only reason is a one-off section that can be handled with a class

You'll also want to think about block naming from day one. A block name is essentially a contract with your content. If you call it wagent/team-grid and later rename it to wagent/team-carousel, you'll break existing content unless you provide a deprecation path. Choose generic, purpose-based names that won't become false advertising as the block evolves. This is a flavor of the naming discipline we've all learned from plugin prefixes, and it applies just as much to block names.

The contrarian take here is the most useful thing I can say: the custom block you build because a client asked for "just one piece" is almost always a mistake. Say no politely, ship a core block with a class, and bank the hours. You'll have more respect from the client—and a smaller line in the maintenance budget.

The "clients will break the editor" objection

This objection is half right. The block editor itself is not the problem; the problem is giving clients too much rope. theme.json can lock down what's editable: disable the template editor, restrict allowed blocks, and set default styles so a misplaced column does less damage. Some clients will still manage to break things, but you can wipe a page back to a saved pattern in one click—something the classic editor couldn't offer.

Let me paint a scenario. A client calls and says, "I moved a section and now the whole page looks wrong." With a classic theme, you'd log in, inspect the CSS, and probably spend an hour fixing the layout. With a block setup, you can open the page, select the content area, and reset it to the saved pattern. The pattern is the baseline; the client's changes are the overlay. When the overlay goes wrong, you remove it. That's not just a nicer workflow; it's a fundamentally more forgiving editor.

Now the nuance: most clients don't want to edit much at all. They want to change text, swap photos, and perhaps reorder a section. The block pattern gives you exactly that without exposing the entire site structure. In this sense, the editor isn't a toy; it's a viewfinder. Your job is to calibrate what clients can see. That means you might disable the "Templates" settings, limit the block inserter to a curated list, and even prefill empty patterns with placeholder work. The editor becomes a content entry form rather than a web design canvas.

On the accessibility side, the block editor's focus management and keyboard support are generally better than a classic editor's template fields. But you still need to ensure that patterns have proper heading hierarchy and accessible names. Since the pattern is shared across clients, you only fix those issues once, which is another hidden win of standardization.

The genuinely hard part is internal. For your team, learning to prototype with blocks requires unlearning the "do it in PHP" habit. That's a real cost, but it's a one-time cost per person. It's not a reason to avoid the approach; it's a reason to start with one pattern library and one forgiving client before rolling out everywhere. Don't let the "my clients can't handle blocks" refrain hide the fact that you haven't yet configured a block setup to meet them halfway.

The "we already have hooks and filters" objection

The principle here is: you're not discarding hooks; you're adding a layer on top. Blocks are the presentation boundary; hooks are still how you inject logic. A dynamic block's render callback runs in PHP, which means you can call the same functions and apply the same filters you already trust.

Imagine a plugin that lets you add a "featured product" field to any post using a filter. With a dynamic block, you can include a server-rendered block that runs that filter and prints the output inside the block's wrapper. The client inserts the block; the existing PHP logic does the heavy lifting. Nothing is thrown away. For an even more concrete example, consider a custom block that lists recent project posts. In its render callback, you call get_posts(), then loop and apply the_title() and the_permalink()—the same template tags you've used for years.

This is also the place to be honest about what doesn't translate. Some clever old themes use template-parts with intricate conditionals that take arguments based on the page context. Recreating that as a block can be messy. But you don't have to recreate it all at once. The incremental path is to keep the PHP logic, wrap it in a dynamic block, and move the markup into the block template. You'll often find your existing filter patterns can handle the new output. And if the logic is tightly coupled to a template hierarchy (e.g., "on search results, show this differently"), you can still use the classic template for those specific views while using blocks for regular pages.

The REST API also opens a different door: you can build blocks that pull data from other WordPress sites or third-party services. A dynamic block can call wp_remote_get() to fetch JSON and render it on the front end. That's a powerful pattern for agency builds where clients want to show social feeds, product listings, or internal data without managing a separate integration. The tradeoff is caching and error handling—if the remote API is slow, your page is slow. Keep API-based blocks out of critical above-the-fold content, or use client-side rendering with a proper loading state.

Actions and filters still run around saving and rendering; the hook architecture doesn't vanish when you adopt blocks, it just moves to a new context. If you need to refresh your understanding of where actions and filters meet this new block world, our hooks deep dive is a useful refresher.

The "FSE isn't production-ready" objection

Fair enough, but ask what "risky" actually means. Full Site Editing has been through several releases, and theme.json has settled into a stable schema. The risk is not that the editor "suddenly breaks"—the risk is that your team's custom code may rely on old-style PHP templates that coexist awkwardly with block templates. Also, some third-party plugins still assume the classic editor or customizer. That's a compatibility decision, not a reason to throw out the whole model.

A useful way to think about it: simple, repeatable sites with content written in blocks are the least risky. High-risk clients are those with deeply customized classic themes or proprietary plugins that render their own front-end. That's a legitimate reason to stick with classic themes for that small niche. The error is pretending that "production-ready" is a single switch that's either on or off.

Before you propose a block theme to a client, run through a quick checklist:

  • Does the client have a heavily customized theme that would require migration?
  • Do the must-have plugins support the Site Editor and REST API?
  • Does the hosting environment allow the file access the block theme expects?
  • Have you set aside time for pattern design, not just block registration?
  • Will the client's team tolerate the editor changes, or do they need a locked template?

If any answer is no, adjust scope or use a hybrid approach. That's not a compromise; it's engineering judgment. And if you're building a hybrid, remember the hooks and filters story above—you can still wrap old logic in dynamic blocks while the theme.json handles the global look.

Versioning your theme.json isn't just a theoretical concern. I've seen an agency's custom block library break when the client updated WordPress and the block's style file registered with wp_register_style() under a changed handle. The fix was easy, but the panic was real. A simple test process—run the update on a staging copy of the site, click through the key pages, then release—solves most of these surprises.

The objection you haven't made to yourself

Here's the meta-objection that keeps agencies from standardizing: "It's a big change, and there's no time to make it during client work." That's true—so don't make it during client work. Pick an internal project or a small client and build one pattern library. Use theme.json as the design-token system. Add a custom block only when it's justified. Wrap the old hooks where they help. Iterate.

Here's a rough first 30 days:

  1. Audit your last five client builds and list the ten most repeated layout pieces.
  2. Turn those ten pieces into block patterns, with a small set of CSS classes.
  3. Build a shared plugin (or mu-plugin) that registers those patterns. If you haven't thought about plugin organization for this, skim this guide on building robust plugins first.
  4. Create one theme.json that matches your baseline design; add client-specific values as you spin up projects.
  5. Pick one small internal project or a friendly client and migrate it to the stack.
  6. Document one hero story of a client who edited their homepage without calling you.

At the end of that experiment, you won't have a "block-first" badge to hang on the wall. You'll have a team that can spin up a new client site from a shared baseline without apologizing for the timeline. You'll also be in a better position to say no to the client's request for a 42nd custom block—because you know exactly what the core blocks can do, or because you can show why a dynamic block would genuinely be faster.

Will you still build some bespoke sites? Yes. Some clients will always need a custom template, a bespoke page, or a proprietary integration that isn't worth forcing into the shared model. The goal isn't to eliminate bespoke work—it's to make it the exception rather than the default.

The repeatability comes from the boring parts: a solid theme.json schema, a clear pattern library, and the discipline to keep the shared layer lean. That's not the shiny version you hear in webinars. It's the one that beats the Monday-morning blank-theme blues.

Sources (5)