Block themes changed what a WordPress theme is allowed to look like. WordPress.org’s own default theme, Twenty Twenty-Three, ships with zero PHP logic — you can confirm that yourself in its GitHub repository. That’s not a gimmick; it reflects a real shift toward themes built almost entirely from blocks and templates rather than template tags and PHP conditionals.
This walkthrough documents a specific, common scenario: you already have a front-end build — in this case, one exported from Webflow, with its own CSS and JavaScript — and you need to wire it into WordPress as a set of custom blocks living inside a block theme, rather than as a separate plugin.

Environment setup
Any local WordPress environment works here; the example uses Laragon, a solid option if you’re also touching other PHP frameworks like Laravel or Symfony. Spinning up a fresh instance in Laragon is a couple of clicks: right-click, Quick app, WordPress.
Starting from a blank block theme
Rather than hand-writing theme scaffolding, install the official Create Block Theme plugin. From Appearance > Create Block Theme, generate a blank theme, then activate it under Appearance > Themes.
Open the project in whatever editor you use — VS Code in this case — ideally pointed at the whole WordPress install rather than just the theme folder, so you can see how the theme sits inside the larger install. At this stage, notice there isn’t a single PHP file anywhere in the theme. That’s expected: a block theme can be assembled entirely through the Site Editor without writing code at all, if that’s all you need. The steps below are for the case where you need custom blocks beyond what the visual editor provides.
Scaffolding blocks inside the theme, not the plugins folder
WordPress’s tooling nudges you toward creating each Gutenberg block as its own plugin. That’s fine for a single reusable block distributed independently, but it’s the wrong shape when you have several blocks that all belong to one theme and one project — which is exactly the case when integrating an existing front-end build. Fortunately, @wordpress/create-block now supports a --no-plugin flag that scaffolds a block without the plugin wrapper, which is the key to keeping everything inside the theme.
Before running anything, confirm your Node version — Node 17 works reliably for this tooling; Node 18 has been known to throw errors in this workflow, so Node 16 or 17 is the safer bet.
From the theme’s folder:
cd fresh-wordpress-install/wp-content/themes/acme
Scaffold a blocks directory to hold everything:
npx @wordpress/create-block blocks --namespace acme
This first run pulls down the tooling, so expect it to take a moment. Once it finishes, you’ll have a blocks folder configured for a single default block — which you’ll now expand into multiple.
Turning one scaffolded block into several
Inside blocks/src, delete the default block’s files (you don’t need a block literally named blocks), but keep the src folder itself, and delete build too — it regenerates automatically from src.
Then scaffold your real blocks, one command per block, each with --no-plugin so it drops into the theme rather than becoming a standalone plugin:
cd blocks/src
npx @wordpress/create-block block1 --namespace acme --no-plugin
npx @wordpress/create-block block2 --namespace acme --no-plugin
With both blocks in place, start the watcher from the blocks folder so changes rebuild automatically:
npm run start
Registering the blocks with WordPress
Two small files connect the scaffolded blocks to the theme. First, rename blocks/blocks.php to init.php and replace its contents with a registration function that points at each block’s build output:
<?php
function acme_blocks_init() {
register_block_type( __DIR__ . '/build/block1' );
register_block_type( __DIR__ . '/build/block2' );
}
add_action( 'init', 'acme_blocks_init' );
Second, create a functions.php file at the theme’s root (acme/functions.php) that pulls this file in:
<?php
require get_theme_file_path('/blocks/init.php');
At this point the folder structure reads as acme/blocks/init.php, wired up through a top-level functions.php — the only PHP the theme needs for registration purposes.
Verifying the blocks work
In the admin panel, go to Appearance > Editor, clear the default content, and insert both blocks by searching their exact names — block1 and block2. Save, then check the public-facing page to confirm both render correctly on the front end.
Going further: server-side rendering for dynamic blocks
Static block output is fine for simple content, but the real payoff of custom blocks shows up when a block’s markup is generated by a PHP render function instead of being baked into the saved post content. Change the render function once, and every instance of that block across the entire site updates — no find-and-replace across hundreds of posts required.
To convert block2 to server-side rendering, add a render.php file under blocks/src/block2/:
<p class="wp-block-acme-block2">
Block2 – hello from PHP!
</p>
Then point the block’s manifest at it by adding a render key to blocks/src/block2/block.json:
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 2,
"name": "acme/block2",
"version": "0.1.0",
"title": "Block2",
"category": "widgets",
"icon": "smiley",
"description": "Example block scaffolded with Create Block tool.",
"supports": {
"html": false
},
"textdomain": "block2",
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css",
"render": "file:../../src/block2/render.php"
}
Reload the front end and you should see the PHP-rendered message in place of the old static markup.
That relative path — file:../../src/block2/render.php — works, but it’s awkward. Clean it up by referencing the file locally instead:
file:./render.php
For that shorter path to resolve correctly at build time, add the --webpack-copy-php flag to every script in blocks/package.json:
{
"name": "blocks",
"version": "0.1.0",
"description": "Example block scaffolded with Create Block tool.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
"main": "build/index.js",
"scripts": {
"build": "wp-scripts build --webpack-copy-php",
"format": "wp-scripts format --webpack-copy-php",
"lint:css": "wp-scripts lint-style --webpack-copy-php",
"lint:js": "wp-scripts lint-js --webpack-copy-php",
"packages-update": "wp-scripts packages-update --webpack-copy-php",
"plugin-zip": "wp-scripts plugin-zip --webpack-copy-php",
"start": "wp-scripts start --webpack-copy-php"
},
"devDependencies": {
"@wordpress/scripts": "^24.5.0"
}
}
Restart the watcher (npm run start from blocks/) and confirm the front end still shows the PHP-rendered output.
Where this leaves you
This setup — a blank block theme, a blocks folder holding multiple --no-plugin blocks, a single init.php registering them, and optional PHP render functions for anything dynamic — is a solid foundation for integrating an existing front-end into WordPress without scattering logic across dozens of small plugins. It’s not exhaustive, but it’s enough to build on for a real project, and the same pattern scales cleanly as you add more blocks over time.