Building a theme by prompt
Scaffolding a Classic Theme
What WP-CLI's theme scaffolder emits in 2026, why no theme.json appears, and how to read twenty generated PHP files before you ship them.
Type wp scaffold _s acme-theme into an agent session and you will get a
theme. Ask the agent for a "modern block theme with theme.json" and the agent, being
helpful, will often run the same command and then tell you it built you a block theme.
The command does not have an option for that. It has never had one.
So the first thing to settle is what the scaffolder is. It is a generator for
Underscores, Automattic's starter classic theme, and Underscores has been a
classic theme since it was written. The output is twenty PHP files, a
style.css, and no theme.json anywhere. If you want a block
theme you are going to write one, and the next lesson builds it from
theme.json outward.
That is not an argument against scaffolding. Classic themes are still correct for a lot of sites, and the PHP template hierarchy is the right model for them. The argument is that the choice has to be made by you, before the prompt, because the tool makes it for you by default and will not tell you it did.
The scaffolder refuses to run until WordPress is installed
This catches people first. Running the command in an empty directory fails with a message about WordPress, not about themes:
$ wp scaffold _s acme-theme
Error: This does not seem to be a WordPress installation.
Pass --path=`path/to/wordpress` or run `wp core download`.
The scaffolder loads WordPress to read its own defaults, so it needs an install, not
just the files. In the lab that means a database and wp core install before
anything else. I built a fresh WordPress 7.0.7 for this lesson and then ran the
scaffolder against it:
$ wp scaffold _s acme-legacy --theme_name="Acme Legacy" --author="Acme" --activate
Success: Created theme 'Acme Legacy'.
Success: Switched to 'Acme Legacy' theme.
Two successes, and no hint about what kind of theme arrived.
Twenty files, and not one of them is theme.json
Here is the whole tree the command produced:
.editorconfig comments.php inc/ sidebar.php
.eslintrc composer.json index.php single.php
.stylelintrc.json footer.php js/ style-rtl.css
404.php functions.php languages/ style.css
LICENSE header.php package.json template-parts/
README.md page.php phpcs.xml.dist
archive.php readme.txt screenshot.png
search.php
Count the PHP files and you get twenty. There is a
template-parts/content.php and its friends, a sidebar.php,
the six classic templates in the hierarchy, and a functions.php that
enqueues a stylesheet. There is no templates/ directory, no
parts/ directory, and no theme.json.
You do not have to take the file listing on trust for the last part. WordPress answers the question directly:
$ wp eval 'var_dump( wp_get_theme( "acme-legacy" )->is_block_theme() );'
bool(false)
WP_Theme::is_block_theme() checks for the templates/index.html
file that only block themes have. Twenty PHP templates and no HTML templates means
false, and that value decides which of two entirely different rendering
paths the site takes. A theme cannot be half of each.
The style.css header is a museum piece
Open the generated style.css and read the header rather than the CSS.
This is what came out of WordPress 7.0.7's own toolchain:
/*!
Theme Name: Acme Legacy
Theme URI: http://underscores.me/
Author: Acme
Author URI: http://underscores.me/
Description: Custom theme: Acme Legacy, developed by Acme
Version: 1.0.0
Tested up to: 5.4
Requires PHP: 5.6
License: GNU General Public License v2 or later
...
*/
Read Tested up to: 5.4 again. WordPress 5.4 shipped in March 2020.
Requires PHP: 5.6 is a version that reached end of life in December 2018.
Neither line is a lie the agent told you; both are what the template ships with today,
and both end up in every theme scaffolded with this command.
What the header lines actually do
Nothing executes differently because of them. WordPress reads
Requires PHP for the admin's compatibility notice and
Tested up to for the theme directory listing, and neither changes how a
template renders. The reason to fix them is that they are the first thing a reviewer
or a plugin compatibility check reads, and they are wrong. It is also a useful
signal: a file that declares a 2020 ceiling was not reviewed by anyone in 2026.
An agent left alone will not notice any of this
Run the same prompt against the same scaffolded theme and the failure is quiet.
Ask for "a responsive theme with a hero section" and the agent edits
header.php, adds a front-page.php, and reports success. Every
edit is correct classic-theme work. The theme is still a classic theme, the
style.css header still says 5.4, and if you asked for
theme.json the agent may write one anyway. A theme.json in a
classic theme is not ignored, but it takes a different path: WordPress applies the
presets and styles, and the PHP templates still render the page. You get half of the
block-theme behaviour and none of the Site Editor.
The reliable check is a file that must be absent and a file that must be present. This is the version I ask an agent to run after any theme work, because it fails loudly in both directions:
$ test -f wp-content/themes/acme-legacy/theme.json && echo "theme.json present"
$ test -d wp-content/themes/acme-legacy/templates && echo "block templates present"
$ find wp-content/themes/acme-legacy -name "*.php" | wc -l
20
Three commands, three facts, no interpretation. Twenty PHP files and no
theme.json is a classic theme. A templates/index.html and a
theme.json is a block theme. Anything else is a hybrid, and hybrids are
where the support tickets come from.
Which path a project should take
The decision is not about novelty. It is about who edits the site afterwards.
| Choose classic when | Choose block when |
|---|---|
| The site is a custom layout that no editor will touch, or the theme is one file of a site you inherited. | An editor needs to change templates, headers, spacing or colours without a deploy. |
| You need to control markup at the PHP level, for example a template that talks to an external API during render. | The design system lives in theme.json: palettes, font sizes,
spacing scales, block styles. |
| A plugin or a page builder owns the front end and the theme is mostly a wrapper. | You want style variations and per-block styles that ship with the theme. |
The block path costs more upfront, because theme.json is a schema with
version-specific behaviour and the agent has less signal about it than about
header.php. The classic path costs more later, because the moment an
editor wants to move a header they are filing a ticket.
Whichever you pick, pick it before the prompt and put it in the prompt as a
constraint with a verification step. "Scaffold a classic theme with
wp scaffold _s, then correct Tested up to and
Requires PHP to the versions we support, and show me the header" is a
prompt an agent can fail. "Build me a theme" is not.
Scaffold a theme against your local WordPress and audit it before you change a line.
wp scaffold _s acme-audit --theme_name="Acme Audit" --author="You" --activate
Then answer four questions with commands rather than with reading:
One: how many PHP files did it create? Two: is it a block theme, according to
WordPress? Three: what are the two version lines in the style.css
header, and what are the current versions of WordPress and PHP on this machine?
Four: which files would you have to delete to make this a block theme, and which
would you have to add?
Write the answers down before you fix anything. The fourth question is the one that tells you which path you are actually on.
Show the solution and the reasoning
The counts depend on your WordPress release, so the commands matter more than my numbers. Against WordPress 7.0.7 the answers were:
$ find wp-content/themes/acme-audit -name "*.php" | wc -l
20
$ wp eval 'var_dump( wp_get_theme( "acme-audit" )->is_block_theme() );'
bool(false)
$ grep -E "Tested up to|Requires PHP" wp-content/themes/acme-audit/style.css
Tested up to: 5.4
Requires PHP: 5.6
$ wp core version; php -r 'echo PHP_VERSION, "\n";'
7.0.7
8.5.6
Question four is the interesting one, and the honest answer is that converting in
place is the wrong move. To make the directory a block theme you would delete all
twenty PHP templates, the inc/ directory, the template-parts/
directory and the enqueuing in functions.php, then add
templates/index.html, templates/single.html,
parts/header.html and a theme.json. That is not a
conversion; it is a new theme that happens to share a
style.css.
So the reasoning is: audit the scaffold, keep the pieces that are genuinely
reusable, and start the block theme in a new directory rather than mutating this one.
The reusable pieces are the licence, the phpcs.xml.dist, the
composer.json if you use it, the screenshot, and the text domain
conventions in the existing PHP. The templates and the version header are not
reusable.
One more thing worth noticing while you are here. wp scaffold _s
generated composer.json and phpcs.xml.dist without being
asked. Read the phpcs.xml.dist before you trust it: the copy this run
produced still configures PHPCompatibilityWP with
testVersion 5.6- and
minimum_supported_wp_version 4.6, and it names the text
domain as my-plugin. A linter configured for a floor you do not support
will pass code you should not ship, and it will not warn you, because from its point
of view 5.6 is the target.
Progress
Mark this lesson done and it stays marked in this browser.