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.
“I have the house to myself this weekend, so I thought I’d try something new. My goal is to build and submit a new free block theme to before Monday. While I work, I’ll describe my dev process in this thread.”
Anders Norén on X (formerly known as Twitter)
About a year ago, Anders Norén posted a 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:
- Reading time
- Like button
- Share buttons
- 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.

Studio app
This was also the first time, Norén used the Studio app by WordPress.com for local development. It’s free and open-source.
First impressions are very good. It probably doesn’t tick all the boxes for people with more advanced needs, but for what I’m doing, it seems perfect.
Anders Norén
WordPress.com hosting
Norén hosts his sites on WordPress.com. Because of the GitHub Deployment feature, he found it to be 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 decision in the Site Editor and save them all back to your theme’s file.
Twenty Twenty Four
“Twenty Twenty-Four is my go-to starting point on most projects these days.”
Anders Norén
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 Noren 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.
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
"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
"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"
}
]
}
Typography
"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
"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
“With the theme settings in place, it’s time to put them to use in block and element-specific styles. This is when the theme starts taking shape. You can do this in the site editor, but it’s faster to edit theme.json directly. Also, many styles can’t be changed in the interface.”, Anders Norén wrote.
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.
Header and Footer
“When I’m building a theme, I almost always start with the header and footer. It fools you into thinking the theme is nearly finished. I’ll wait with the newsletter form and the other Jetpack blocks until all the templates are in place.” — Norén shared in his thread.

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 that the text wrapped in esc_html_e()
function can be translated. See below an example of a group of Navigation links.
Template part: header.html
<!-- wp:pattern {"slug":"pulitzer/hidden-header"} /-->
Click the arrow to see the Pattern code
<?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
“For the line clamped excerpts, I registered custom block styles for the “Excerpt” block and enqueues a CSS file when the block is in use on the page. You can do a lot with
theme.json
styles, but stuff like this still requires CSS.” Norén found.
Register_block_style
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
[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;
}

For a basic tutorial 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.
“It’s a shame that you can’t use the “Choose a header/footer” modal for custom template part areas yet. It would make switching between template parts a lot more intuitive. There’s an open issue for it here: Add support for replacing “general” template parts.” Norén discovered.
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
Header
404 Pattern
Search Pattern
Footer
Search Pattern.
<?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
/**
* 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
<!-- 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"} /-->

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()
/**
* 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 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.
Wow. Those of you who said the new Block bindings API is easy to use weren’t kidding. It took me all of 40 mins to get the reading time and copyright year blocks up and running, having never touched it before. Game changer.
Anders Norén
The two blog post that helped Norén to catch up on the feature:
- Introducing Block Bindings, part 1: connecting custom fields by Justin Tadlock
- New Feature: The Block Bindings API by Artemio Morales.
The php code is in functions.php, starting line 240
The copyright year in the footer
Step one: register the block binding and its callback in functions.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.
/*
* 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
<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"pulitzer/copyright-year"}}}} -->
<p><?php esc_html_e('© [year]', 'pulitzer');?></p>
<!-- /wp:paragraph -->

Reading Time
Justin Tadlock kindly offered Norén to use his code for calculating reading time.
I’ve been using his PHP snippets for 15+ years, so why stop now? — Norén
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
“I decided to add one for the comments count link on the archive pages as well. It’s crazy that a “X comments” block linking to the comments field hasn’t landed in Core yet. (It’s in Gutenberg). Slippery slope? Maybe, but I’m on a deadline, so that’s for another day.” — Anders Norén
* 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.
“Rare is the block theme where you don’t end up creating at least one issue in the Gutenberg repo, or think you ought to. Turns out the
hasIcon:false
setting for the navigation block only works if the navigation doesn’t have any styles set. (61181)” – Anders Norén.
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, Anders 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 page – Code on GitHub
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”
{
"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
I’ve done another round of cleanup and polish, received the all-clear from the theme check plugin, added a proper theme description and created an info page about the theme on my site.
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
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.
I also want to say thank you to everyone who has reached out to say they appreciate the thread. I wasn’t sure if this would end up at all useful or interesting to anyone, so it means a lot.
Anders Norén
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?
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-fi, GitHub, 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.
The post Gutenberg Times: Seven Tasks to a Custom Block Theme: Anders Norén’s Weekend Workflow appeared first on MCNM Digital Media Marketing.