What we will build
In this section you will build a small block theme for a bakery, step by step. A block theme needs surprisingly little:
wp-content/themes/bakery/
├── style.css the theme header (and optional CSS)
├── theme.json settings and styles (next lesson)
├── templates/
│ └── index.html the fallback template for every page
└── parts/
├── header.html shared header
└── footer.html shared footerOnly style.css and templates/index.html are required. The rest makes the theme pleasant to build on.
style.css: the theme header
WordPress identifies a theme by the comment at the top of style.css:
/*
Theme Name: Bakery
Theme URI: https://example.com/bakery
Author: Your Name
Description: A small block theme for a neighbourhood bakery.
Version: 1.0.0
Requires at least: 6.6
Requires PHP: 7.4
License: GNU General Public License v2 or later
Text Domain: bakery
*/Theme Name is the only required field. Text Domain is used for translations and should match the folder name. Block themes usually keep style.css for the header only and put their design in theme.json.
theme.json
A minimal theme.json sets a content width, so text lines are not too long to read:
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"settings": {
"layout": { "contentSize": "680px", "wideSize": "1100px" }
}
}The $schema line gives code editors autocomplete and validation. The next lesson is all about this file.
Template parts
A template part is a piece shared by many templates. The header shows the site title (linked to the home page) and the tagline:
<!-- wp:group {"tagName":"header","layout":{"type":"flex","justifyContent":"space-between"}} -->
<header class="wp-block-group">
<!-- wp:site-title {"level":0} /-->
<!-- wp:site-tagline /-->
</header>
<!-- /wp:group --><!-- wp:group {"tagName":"footer"} -->
<footer class="wp-block-group">
<!-- wp:paragraph -->
<p>Fresh bread, every morning from 7am.</p>
<!-- /wp:paragraph -->
</footer>
<!-- /wp:group -->These files contain exactly the block markup you saw in the block editor lesson. Site Title and Site Tagline are dynamic blocks: they read the current values from Settings → General each time the page is shown. The Site Title has {"level":0}: by default it renders as an h1 heading on every page, which would give each post page two top-level headings (the site title and the post title). Level 0 makes it a plain paragraph, so the page's own title can be the single h1.
The index template
templates/index.html is the fallback template: WordPress uses it for any page that has no more specific template (the template hierarchy lesson explains the order). It pulls in the header, lists posts with a Query Loop, and adds the footer:
<!-- wp:template-part {"slug":"header","tagName":"header"} /-->
<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">
<!-- wp:query {"query":{"inherit":true}} -->
<div class="wp-block-query">
<!-- wp:post-template -->
<!-- wp:post-title {"isLink":true} /-->
<!-- wp:post-excerpt /-->
<!-- /wp:post-template -->
<!-- wp:query-no-results -->
<!-- wp:paragraph --><p>Nothing found.</p><!-- /wp:paragraph -->
<!-- /wp:query-no-results -->
</div>
<!-- /wp:query -->
</main>
<!-- /wp:group -->
<!-- wp:template-part {"slug":"footer","tagName":"footer"} /-->Key parts:
wp:template-part {"slug":"header"}loadsparts/header.html.wp:query {"query":{"inherit":true}}is a Query Loop that uses the main query: whatever WordPress decided this URL should show (the latest posts on the home page, one post on a single post page, matching posts on a category page).wp:post-templaterepeats its inner blocks for each post; here a linked title and an excerpt.wp:query-no-resultsis shown when nothing matches.- The
constrainedlayout onmainapplies thecontentSizefromtheme.json.
Activate it
The theme now appears in Appearance → Themes. Activate it there, or:
wp theme activate bakery
wp theme list --fields=name,status,versionSuccess: Switched to 'Bakery' theme.
+-------------------+----------+---------+
| name | status | version |
+-------------------+----------+---------+
| bakery | active | 1.0.0 |
| twentytwentyfive | inactive | 1.5 |
| twentytwentyfour | inactive | 1.6 |
| twentytwentythree | inactive | 1.7 |
+-------------------+----------+---------+Let's add a tagline and look at the home page. The header and footer extracts show the template parts' text, and the main extract shows what the Query Loop produced:
wp option update blogdescription "Baked fresh daily"Success: Updated 'blogdescription' option.GET /My WordPress Site
Baked fresh dailyGET /Hello world!
Welcome to WordPress. This is your first post. Edit or delete it, then start writing!GET /Fresh bread, every morning from 7am.The home page lists the one post a new site has ("Hello world!") with its excerpt, between our header and footer. Because there is only an index.html template, the single post page, pages and archives all use the same layout for now; the templates lesson adds proper ones.
The theme in the editor
Open Appearance → Editor and you can edit this theme visually. When you save a change to a template or part, WordPress stores a customised copy in the database (as in the Site Editor lesson), leaving your files untouched. While developing, remember to reset customisations or export them back into your files, or you may wonder why your file changes do not show.
Screenshot
Themes usually include a screenshot.png (1200 by 900 pixels) in the theme folder, which Appearance → Themes shows as the preview. Without one, the theme shows a blank card.
Common mistakes
- Missing
templates/index.html, so WordPress treats the folder as a broken theme. - Invalid block markup: an unclosed
<!-- wp:group -->comment breaks the layout. Build templates in the Site Editor and export them if you are unsure. - Forgetting
{"inherit":true}on the main Query Loop, so every page shows the same latest posts. - Editing files while a customised copy exists in the database and wondering why nothing changes.
Exercise
Add a second paragraph to the footer part with a link to a page called "Contact", and make the header show the tagline only (remove the site title). Check the home page.
Show one possible answer
In parts/footer.html, inside the footer group, add:
<!-- wp:paragraph -->
<p><a href="/contact/">Contact us</a></p>
<!-- /wp:paragraph -->In parts/header.html, delete the line <!-- wp:site-title {"level":0} /-->. Reload the home page: the header shows "Baked fresh daily" and the footer has the new link. If nothing changes, check Appearance → Editor for a customised copy of the part and reset it.
Key points
- A block theme is a folder in wp-content/themes with at least style.css (the theme header) and templates/index.html.
- Templates are HTML files made of block markup; template parts such as parts/header.html are shared pieces.
- theme.json configures settings and styles; it is optional but nearly every block theme has one.
- WordPress reads the theme header comment in style.css to list the theme in Appearance → Themes.