Seven Tasks to a Custom Block Theme: Anders Norén’s Weekend Workflow

Have you ever wondered how a modern WordPress block theme comes together – from first sketches to a polished, production-ready design? Over a single weekend, Anders Norén built Pulitzer, a new block theme, from the ground up. In this post, I’ll walk you through Anders’ process. I’ll highlight the tools, decisions, and little tricks that helped him move quickly.

About a year ago, Anders Norén posted an X (formerly known as Twitter) thread about his process. He gave me permission to collect the tweets into a blog post. Since then, he deleted his account and content on X. The valuable information is not lost. Here you go.

The inspiration and seven tools

The theme Norén set out to build that weekend is called Pulitzer. It is meant for long-form writing with a special consideration for writers with newsletters.

Figma

You can find a Figma presentation here. The video walks you through the Pulitzer Figma space.

Jetpack and Block Bindings

Some elements in the design stand out:

  1. Reading time
  2. Like button
  3. Share buttons
  4. Newsletter signup

For a self-hosted WordPress site, those blocks are not available out of the box unless you install the Jetpack plugin. For the reading time, comment count and copyright year in the footer, Norén experimented with the Block Bindings API.

Mockup of a single post representation in a list of post with red time, number of likes and number of comments

Studio app

This was also the first time, Norén used WordPress Studio for local development. It’s free and open-source.

Screenshot of Studio, the free and open-source local development tool by WordPress.com

WordPress.com hosting

Norén hosts his sites on WordPress.com. Because of the GitHub Deployment feature, he found it easy to keep the Pulitzer demo site updated.

Create Block Theme

Another tool he used is the community plugin Create Block theme. Once installed, it helps you make design decisions in the Site Editor and save them back to your theme’s file.

Twenty Twenty-Four

He also found that it’s probably the best default theme ever. He gave a special shout-out to the theme leads Jessica Lyschik and Maggie Cabrera.

Both the demo and the GitHub repo are publicly accessible:

With all the tools in place, Anders Norén ventured to build the WordPress theme.

First task: remove many things

Norén began by taking the Twenty-Twenty-Four default theme. He started with removing all the templates, template parts, patterns, fonts, images, and styles that won’t be needed. Then, he renamed the rest.

Screenshot of the list of files

Second task: update theme.json

In a second step, Norén updated the theme settings with those from the design in the theme.json file

  • spacing sizes,
  • colors, and
  • typography

Spacing and Colors

HTML
"spacing": {
			"spacingScale": {
				"steps": 0
			},
			"spacingSizes": [
				{
					"name": "4px",
					"size": "4px",
					"slug": "10"
				},
				{
					"name": "8px",
					"size": "8px",
					"slug": "20"
				},
				{
					"name": "12px",
					"size": "12px",
					"slug": "30"
				},
				{
					"name": "16px",
					"size": "16px",
					"slug": "40"
				},
				{
					"name": "24px",
					"size": "24px",
					"slug": "50"
				},
				{
					"name": "32px",
					"size": "32px",
					"slug": "60"
				},
				{
					"name": "48px",
					"size": "clamp(32px, 4.8vw, 48px)",
					"slug": "70"
				},
				{
					"name": "64px",
					"size": "clamp(48px, 6.4vw, 64px)",
					"slug": "80"
				},
				{
					"name": "96px",
					"size": "clamp(64px, 9.6vw, 96px)",
					"slug": "90"
				},
				{
					"name": "128px",
					"size": "clamp(64px, 12.8vw, 128px)",
					"slug": "100"
				},
				{
					"name": "Body Margin (24px)",
					"size": "24px",
					"slug": "body-margin"
				}
			],
			"units": [
				"%",
				"px",
				"em",
				"rem",
				"vh",
				"vw"
			]
		}
Click to see more
HTML
"color": {
			"defaultPalette": false,
			"palette": [
				{
					"color": "#FFFFFF",
					"name": "Base",
					"slug": "base"
				},
				{
					"color": "#F9F9F9",
					"name": "Base / Two",
					"slug": "base-2"
				},
				{
					"color": "#191716",
					"name": "Contrast",
					"slug": "contrast"
				},
				{
					"color": "#666666",
					"name": "Contrast / Two",
					"slug": "contrast-2"
				},
				{
					"color": "#767676",
					"name": "Contrast / Three",
					"slug": "contrast-3"
				},
				{
					"color": "#DADADA",
					"name": "Contrast / Four",
					"slug": "contrast-4"
				},
				{
					"color": "#EEEEEE",
					"name": "Contrast / Five",
					"slug": "contrast-5"
				}
			]
		}
Click to see more

Typography

HTML
"fontFamilies": [
				{
					"fontFace": [
						{
							"fontFamily": "Newsreader",
							"fontStretch": "normal",
							"fontStyle": "normal",
							"fontWeight": "200 900",
							"src": [
								"file:./assets/fonts/newsreader/newsreader-var.woff2"
							]
						},
						{
							"fontFamily": "Newsreader",
							"fontStretch": "normal",
							"fontStyle": "italic",
							"fontWeight": "200 900",
							"src": [
								"file:./assets/fonts/newsreader/newsreader-var-italic.woff2"
							]
						}
					],
					"fontFamily": "\"Newsreader\", ui-serif, \"Times New Roman\", serif",
					"name": "Newsreader",
					"slug": "body"
				},
				{
					"fontFamily": "ui-sans-serif, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", sans-serif",
					"name": "System Sans-serif",
					"slug": "system-sans-serif"
				},
				{
					"fontFamily": "ui-serif, \"Times New Roman\", serif",
					"name": "System Serif",
					"slug": "system-serif"
				}
			],
Click to see more
HTML
"fontSizes": [
				{
					"fluid": false,
					"name": "XXS",
					"size": "12px",
					"slug": "xx-small"
				},
				{
					"fluid": false,
					"name": "XS",
					"size": "14px",
					"slug": "x-small"
				},
				{
					"fluid": false,
					"name": "Small",
					"size": "16px",
					"slug": "small"
				},
				{
					"fluid": false,
					"name": "Medium",
					"size": "18px",
					"slug": "medium"
				},
				{
					"fluid": false,
					"name": "Large",
					"size": "21px",
					"slug": "large"
				},
				{
					"fluid": {
						"max": "24px",
						"min": "21px"
					},
					"name": "XL",
					"size": "24px",
					"slug": "x-large"
				},
				{
					"fluid": {
						"max": "32px",
						"min": "24px"
					},
					"name": "XXL",
					"size": "32px",
					"slug": "xx-large"
				},
				{
					"fluid": false,
					"name": "Massive",
					"size": "clamp( 96px, 19.2vw, 128px )",
					"slug": "massive"
				}
			],
			"writingMode": true
		}
Click to see more

As the new theme, Pulitzer doesn’t have any working templates yet, Norén checked the theme.json styles in the Site editor Stylebook view. You get the vibe of the theme. You can also use it to make sure you haven’t forgotten any styling for core blocks.

Third task: Templates and Patterns

This is the moment to work on the theme layouts. Norén uses what he calls “the one indispensable tool in the Block Theming toolbox,” the Create Block Theme (CBT) plugin.

  • Step 1: Make the changes in the site editor.
  • Step 2: Save them to the theme with CBT.

Working in the Site Editor

Saving changes to the theme with CBT


The header template part is only the container for the hidden-header pattern. The reason to use patterns is that you can add php code. The advantage is that the text wrapped in esc_html_e() function can be translated. See below an example of a group of Navigation links.

Screenshot of the code building the navigation links

Template part: header.html

PHP
<!-- wp:pattern {"slug":"pulitzer/hidden-header"} /-->

hidden-header.php

Click the arrow to see the Pattern code
PHP
<?php
/**
 * Title: header
 * Slug: pulitzer/hidden-header
 * Inserter: no
 */
?>
<!-- wp:group {"align":"wide","style":{"spacing":{"padding":{"top":"var:preset|spacing|60","bottom":"var:preset|spacing|60"}},"border":{"bottom":{"color":"var:preset|color|contrast-5","width":"1px"},"top":[],"right":[],"left":[]}},"layout":{"type":"constrained"}} -->
<div class="wp-block-group alignwide" style="border-bottom-color:var(--wp--preset--color--contrast-5);border-bottom-width:1px;padding-top:var(--wp--preset--spacing--60);padding-bottom:var(--wp--preset--spacing--60)">

	<!-- wp:columns {"isStackedOnMobile":false,"style":{"spacing":{"blockGap":{"left":"var:preset|spacing|50"}}}} -->
	<div class="wp-block-columns is-not-stacked-on-mobile">
		
		<!-- wp:column {"verticalAlignment":"stretch"} -->
		<div class="wp-block-column is-vertically-aligned-stretch">
			<!-- wp:group {"style":{"dimensions":{"minHeight":"100%"},"spacing":{"blockGap":"var:preset|spacing|40"}},"layout":{"type":"flex","orientation":"vertical","verticalAlignment":"space-between"}} -->
			<div class="wp-block-group" style="min-height:100%">
				<!-- wp:group {"style":{"spacing":{"blockGap":"var:preset|spacing|10"},"layout":{"selfStretch":"fit","flexSize":null}},"layout":{"type":"flex","orientation":"vertical"}} -->
				<div class="wp-block-group">
					<!-- wp:site-title {"level":0} /-->
					<!-- wp:site-tagline /-->
				</div>
				<!-- /wp:group -->

				<!-- wp:navigation {"hasIcon":false,"layout":{"type":"flex","orientation":"horizontal"}} -->

				<!-- wp:navigation-link {"label":"<?php esc_html_e( 'Blog', 'pulitzer' ); ?>","url":"#"} /-->
				<!-- wp:navigation-link {"label":"<?php esc_html_e( 'Profile', 'pulitzer' ); ?>","url":"#"} /-->
				<!-- wp:navigation-link {"label":"<?php esc_html_e( 'Newsletter', 'pulitzer' ); ?>","url":"#"} /-->

				<!-- /wp:navigation -->

			</div>
			<!-- /wp:group -->
		</div>
		<!-- /wp:column -->

		<!-- wp:column {"width":"1em","layout":{"type":"constrained","justifyContent":"right"},"fontSize":"massive"} -->
		<div class="wp-block-column has-massive-font-size" style="flex-basis:1em">
			<!-- wp:site-logo {"width":128,"shouldSyncIcon":true,"className":"is-style-rounded"} /-->
		</div>
		<!-- /wp:column -->

	</div>
	<!-- /wp:columns -->

</div>
<!-- /wp:group -->

The same system applied to the template part footer.html that includes the hidden-footer.php.

Archives

Register_block_style

HTML
register_block_style(
			'core/post-excerpt',
			array(
				'name'	=> 'pulitzer-clamp-lines-2',
				'label'	=> __( 'Clamp: 2 lines', 'pulitzer' )
			)
		);

		register_block_style(
			'core/post-excerpt',
			array(
				'name'	=> 'pulitzer-clamp-lines-3',
				'label'	=> __( 'Clamp: 3 lines', 'pulitzer' )
			)
		);

CSS for post/excerpt clamp lines styles

HTML
[class*="is-style-pulitzer-clamp-lines-"] p:first-child {
	display: -webkit-box;
	-webkit-box-orient: vertical;  
	overflow: hidden;
}

.is-style-pulitzer-clamp-lines-2 p:first-child {
	-webkit-line-clamp: 2;
}

.is-style-pulitzer-clamp-lines-3 p:first-child {
	-webkit-line-clamp: 3;
}
List of post with a read time block and a standardized excerpt block

For basic steps on block styles, you should read my tutorial. It is titled Mastering Custom Block Styles in WordPress: 6 Methods for Theme and Plugin Developers.

Alternative post layouts

Norén followed a tutorial on the WordPress Developer Blog: Upgrading the site-editing experience with custom template part areas by Justin Tadlock. He added different post layouts as template parts. They are registered to a custom “posts” template parts area.

Examples of the modal to select a header pattern

The extra post layouts were created as patterns to be added into the respective template parts.

The patterns are in separate files in the patterns folder prefixed with hidden-posts– with the settings: Categories: hidden and Inserter: no

404-template and the Search block

For the 404-template, the search form is loaded as a hidden pattern, both here and in the search template. This ensures that styling and translatable strings stay consistent.

This is a great example for nesting template parts and patterns.

The Search pattern is the smallest unit. It is included in the 404 pattern. Then, with the header and footer template parts, it is included in the 404.html template.

The 404 Template schematic

404 Pattern

Search Pattern

Search Pattern

PHP
<?php
/**
 * Title: Search
 * Slug: pulitzer/hidden-search
 * Inserter: no
 */
?>
<!-- wp:search {
							 "label":"<?php echo esc_attr_x( 'Search', 'search form label', 'pulitzer' ); ?>",
               "showLabel":false,
               "placeholder":"<?php echo esc_attr_x( 'Search for...', 'search form placeholder', 'pulitzer' ); ?>",
               "buttonText":"<?php echo esc_attr_x( 'Search', 'search button text', 'pulitzer' ); ?>",
               "buttonPosition":"button-inside",
               "buttonUseIcon":true
                } /-->

404-Page Pattern

PHP
<?php
/**
 * Title: 404
 * Slug: pulitzer/hidden-404
 * Inserter: no
 */
?>
<!-- wp:group {"style":{"spacing":{"blockGap":"var:preset|spacing|40"}},"layout":{"type":"constrained","contentSize":"21em"}} -->
<div class="wp-block-group">
	<!-- wp:heading {"textAlign":"center","level":1} -->
	<h1 class="wp-block-heading has-text-align-center" id="page-not-found"><?php echo esc_html_x( 'Error 404', 'Heading for a webpage that is not found', 'pulitzer' ); ?></h1>
	<!-- /wp:heading -->

	<!-- wp:paragraph {"align":"center"} -->
	<p class="has-text-align-center"><?php echo esc_html_x( 'We can’t find the page you’re looking for. Go back to the front page, or try the search form below.', 'Message to convey that a webpage could not be found', 'pulitzer' ); ?></p>
	<!-- /wp:paragraph -->
</div>
<!-- /wp:group -->

<!-- wp:group {"layout":{"type":"constrained","contentSize":"240px"}} -->
<div class="wp-block-group">
	<!-- wp:pattern {"slug":"pulitzer/hidden-search"} /-->
</div>
<!-- /wp:group -->

404-Page Template

PHP
<!-- wp:template-part {"slug":"header","area":"header","tagName":"header"} /-->

<!-- wp:group {"tagName":"main","align":"full","style":{"spacing":{"padding":{"top":"var:preset|spacing|100","bottom":"var:preset|spacing|100"}}},"layout":{"type":"constrained"}} -->
<main class="wp-block-group alignfull" style="padding-top:var(--wp--preset--spacing--100);padding-bottom:var(--wp--preset--spacing--100)">
	<!-- wp:pattern {"slug":"pulitzer/hidden-404"} /-->
</main>
<!-- /wp:group -->

<!-- wp:template-part {"slug":"footer","area":"footer","tagName":"footer"} /-->
Screenshot of the 404-Page in the Pulitzer theme

Fourth task: handling blocks

There are specific blocks outside core blocks that need more than styling. Some php code will definitely be involved:

Jetpack blocks

For Jetpack blocks, conditional output is simple since the plugin registers blocks only when modules are active. Norén implemented a helper function to check if blocks are registered before using them in pattern PHP files.

In functions.php Norén created a helper function to check if a certain block is available pulitzer_is_block_registered().

PHP
/**
 * Check if a block is registered.
 */
if ( ! function_exists( 'pulitzer_is_block_registered' ) ) :
	/**
	 * Check if a block is registered
	 *
	 * @since Pulitzer 1.0
	 * @return bool
	 */
	function pulitzer_is_block_registered( $block_name ) {
		$registry = WP_Block_Type_Registry::get_instance();
 		return $registry->get_registered( $block_name );
	}
endif;

This helper function is then available for the conditional check in the pattern:

Examples for the jetpack/like button. You can inspect the whole code for the hidden-single sharing-row pattern on GitHub.

PHP
<?php if ( pulitzer_is_block_registered( 'jetpack/like' ) ) : ?>
			<!-- wp:group {"style":{"spacing":{"padding":{"top":"6px"}}}} -->
			<div class="wp-block-group" style="padding-top:6px">
				<!-- wp:jetpack/like /-->
			</div>
			<!-- /wp:group -->
<?php endif; ?>

For the like button, share buttons and newsletter signup, he utilized styled versions of Jetpack blocks. Using block stylesheet registration ensures the CSS is loaded only when a block is in use.

Block Bindings API blocks

In the final version, Pulitzer includes three use cases of the Block Binding API:

  • Number of comments on a post, with a link to the post comments form.
  • Reading time of a post.
  • Current year next to the copyright note in the footer.

The two blog posts that helped Norén to catch up on the feature:

The php code is in functions.php, starting line 240

Step one: register the block binding and its callback in functions.php.

PHP
function pulitzer_register_block_bindings() {
/*
		 * Copyright character with current year.
		 */
		register_block_bindings_source( 
			'pulitzer/copyright-year', 
			array(
				'label'              => __( 'Copyright year', 'pulitzer' ),
				'get_value_callback' => 'pulitzer_block_binding_callback_copyright_year'
			)
		);
}
add_action( 'init', 'pulitzer_register_block_bindings' );

Step two: create the callback function reference in the step before.

pulitzer_block_binding_callback_copyright_year
/*
 * Block bindings callback:
 * Copyright character with current year.
 */
if ( ! function_exists( 'pulitzer_block_binding_callback_copyright_year' ) ) :
	/**
	 * Block bindings callback
	 * Copyright character with current year
	 *
	 * @since Pulitzer 1.0
	 * @return string
	 */
	function pulitzer_block_binding_callback_copyright_year() {
		return '© ' . date( 'Y' );
	}
endif;

Step three: add the block to the pattern.

HTML
<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"pulitzer/copyright-year"}}}} -->
			<p><?php esc_html_e('© [year]', 'pulitzer');?></p>
<!-- /wp:paragraph -->
Close up of the dynamic Copyright block in the footer of the Pulitzer theme

Reading Time

Justin Tadlock kindly offered Norén to use his code for calculating reading time.

Functions.php is your first location to find out how this block binding was created. You can also see what the callback function does. Then, look into any of the post patterns to see how it is used there.

Comments count

Single post view of the list of posts of the Pulitzer theme
PHP
 * Block bindings callback:
 * Post comments count.
 */

if ( ! function_exists( 'pulitzer_block_binding_callback_post_comments_count' ) ) :
	/**
	 * Block bindings callback
	 * Post comments count.
	 *
	 * @since Pulitzer 1.0
	 * @return string
	 */
	function pulitzer_block_binding_callback_post_comments_count( array $source_args, WP_Block $block_instance, string $attribute_name ) {
		$post_id = $block_instance->context['postId'] ?? get_the_ID();

		if ( ! comments_open( $post_id ) ) return false;

		$comments_link = '<a class="pulitzer-comment-count-link" href="' . esc_url( get_comments_link( $post_id ) ) . '">';
		$comments_link .= '<span class="count">' . esc_html( get_comments_number( $post_id ) ) . '</span>';
		$comments_link .= '</a>';

		return $comments_link;

	}
endif;

It’s one of the rare moments you need to look into the theme’s style.css to find the styling for the comment count bubble.

Fifth Task: Patterns

In block themes, patterns are simply PHP files in the /patterns/ folder.

You can study the code for the Patterns by following the GitHub links. As mentioned above, Norén uses small php snippets with his text strings, to allow for translations.

Here is an example:

<!-- wp:paragraph {"fontSize":"large"} -->
<p class="has-large-font-size"><?php esc_html_e( 'I have a long and storied career in the newspaper and publishing industry behind me. Testimonials are available by request.', 'pulitzer' ); ?></p>
<!-- /wp:paragraph -->

Learn more about preparing a theme to be used with multiple languages in the Theme Handbook > Advanced topics > Internationalization.

The newsletter page pattern

See Demo pageCode on GitHub

Close up of the Newsletter signup pattern of the Pulitzer theme

The resume page pattern

The resume list is separate. It can be added on its own to an existing page. It can also be modified for a different historical timeline.

See Demo Page | Code view on GitHub | Code Resume List Pattern

Contact Page Pattern

Patterns are PHP files. You can use loops to output recurring block layouts. This includes layouts like the stack of five columns used to list contact approaches in the Contact page pattern. It makes the patterns easier to maintain.

On GitHub: Contact List Pattern | Contact Page Pattern

Sixth Task: Style Variations

On to theme style variations! These are included as /styles/[name].json files in block themes. Users can select them at Editor → Styles. Theme style variations can modify just about anything set in theme.json, but Norén was sticking to a single simple Inverted style for 1.0. Later he added two more styles, “Humanist” and “Parchment.”

Screenshot of Pulitzer Theme with Inverted Style Variation.
Inverted.json
{
	"settings": {
		"color": {
			"palette": [
				{
					"color": "#111111",
					"name": "Base",
					"slug": "base"
				},
				{
					"color": "#161616",
					"name": "Base / Two",
					"slug": "base-2"
				},
				{
					"color": "#FFFFFF",
					"name": "Contrast",
					"slug": "contrast"
				},
				{
					"color": "#7F7F7F",
					"name": "Contrast / Two",
					"slug": "contrast-2"
				},
				{
					"color": "#616161",
					"name": "Contrast / Three",
					"slug": "contrast-3"
				},
				{
					"color": "#4A4A4A",
					"name": "Contrast / Four",
					"slug": "contrast-4"
				},
				{
					"color": "#222222",
					"name": "Contrast / Five",
					"slug": "contrast-5"
				}
			]
		}
	},
    "title": "Inverted",
	"$schema": "https://schemas.wp.org/trunk/theme.json",
    "version": 2
}

Seventh Task: Submit to the Repository

Screenshot of the Result of automated Theme Scanning.

For more detailed information, you can find in the Theme Handbook page: Submitting Your Theme to WordPress.org.

Pulitzer is available in the WordPress Theme repository

Final full page view of Pulitzer theme.

After the X (formerly known as Twitter) Thread was published, Anders Norén wrote a blog post almost exactly a year ago. The post introduced the Pulitzer theme.

Share what your process looks like in the comments, also share your challenges working with block themes, or what you learn on the way. You can also join us on Discord to discuss with other theme and block developers.


Who is Anders Norén?

Avatar: Anders Norén, as seen on his WordPress profile

Andres Norén is a freelance designer & developer living in the Swedish mountains. You can now follow him on Bluesky, or read his blog.

Eleven years ago, Norén published his first Theme in the WordPress repository, Wilson in 2014. There are now 33 Themes by him available.

He has been an early adopter of block themes with his theme Tove, first released in September 2021. In January of this year, he released his twelfth block theme: Speakermann. You can take a look at all block themes by Norén in the repository.

You can support Anders Norén and his work by sponsoring him on Ko-fiGitHub, or PayPal.

Anders Norén was also a guest on the Gutenberg Time Live Q & A.
He discussed the transition from Classic Themes to block-based Themes together with Carolina Nymark and Ellen Bauer. This took place on October 21, 2021.

7 Comments