Tech

Reviving a WordPress theme

I originally created the WordPress theme for this site back in 2010 when WordPress was on version 3.0 (and Kesha’s “Tik Tok” was Billboard’s top single of the year). Then I pretty much ignored it for 16 years. Thankfully, DreamHost was keeping my WordPress installation up to date so this site didn’t become a pawn in the dark web. And, much to my surprise, my theme still worked fine 4 major versions later. It wasn’t great… but it worked.

Block vs. classic vs. hybrid

As part of reviving this site, I figured I should give the theme a refresh. Over those 16 years, WordPress had introduced block themes while still supporting classic themes 1. Meanwhile, the community had merged the two concepts into a Frankenstein-like hybrid theme. It took a surprising amount of combing through opinions on the 3 types to figure out which I should use. To me, it ultimately feels like block themes are best for people who sell/distribute their themes, but everyone else should probably stick to the simpler, more powerful classic or hybrid variants. Between those 2, hybrid themes offer a tiny advantage when it comes to controlling the Gutenberg block editor, which mostly comes down to having a theme.json file with some settings 2.

Theme folder structure

Here’s the structure I ended up with 3:

% tree --dirsfirst -I "node_modules|.git"
.
├── assets
│   ├── css
│   │   ├── admin.css
│   │   ├── editor-style.css
│   │   └── main.css
│   ├── fonts
│   │   ├── ...
│   ├── images
│   │   ├── ...
│   ├── js
│   │   ├── editor.js
│   │   ├── global.js
│   │   └── global.min.js
│   └── scss
│       ├── abstracts
│       │   ├── ...
│       │   └── _theme.scss
│       ├── base
│       │   ├── ...
│       ├── components
│       │   ├── ...
│       ├── layout
│       │   ├── ...
│       ├── pages
│       │   ├── ...
│       ├── vendors
│       │   └── ...
│       ├── admin.scss
│       ├── editor-style.scss
│       └── main.scss
├── blocks
│   └── pin-photo
│       ├── jsx
│       │   └── ...
│       └── ...
├── inc
│   └── ...
├── page-templates
│   └── ...
├── ...
├── build-tokens.js
├── ...
├── package.json
├── ...
├── style.css
├── theme.json
├── tokens.json
└── webpack.config.js

Aspects to call out here:

  • I moved from a single massive style.js CSS file to SASS files in the 7-1 pattern 4. Each folder has an _index.scss file that includes everything in that folder so that my top-level main.scss just has to @use each of the 6 folders. This also lets me create an editor-style.scss file that includes parts of these without copying styles. The required style.css file now just has the WordPress comment block providing the meta details about the theme and isn’t actually used on the frontend anywhere.
  • Again to avoid repetition, my design tokens, including colors, fonts, dimensions, and spacing, are defined in a generic tokens.json that gets auto-generated by build-tokens.js into theme.json and assets/scss/abstracts/_theme.scss with everything as :root variables.
  • I have a build command defined in package.json to run build-tokens.js, build the SASS files into minified CSS, minify the JavaScript files, build the custom pin-photo block, and ZIP the necessary files into an archive ready for upload on WordPress. That final step has been a huge time saver and avoids uploading my raw SASS files or any of the build scripts that just don’t need to be available online.
  • The webpack.config.js file is necessary because @wordpress/build has a nasty bug that wipes out everything in its target folder. This turns output.clean to false to stop that from happening.

Making WordPress leaner

With the Gutenberg block editor, WordPress is now inserting a ton of inline styles into every page. Since I’m styling everything, I end up not needing any of that and sometimes had to litter my styles with !important to bypass them since inline styles take precedence over external stylesheets. So, in my theme, I nuke everything WordPress tries to add:

function theme_dequeue_block_styles() {
    wp_dequeue_style( 'wp-block-library' );
    wp_dequeue_style( 'wp-block-library-theme' );
    wp_dequeue_style( 'classic-theme-styles' );
    wp_dequeue_style( 'global-styles' );
}
add_action( 'wp_enqueue_scripts', 'theme_dequeue_block_styles', 100 );

function theme_remove_global_styles() {
    add_filter( 'should_load_separate_core_block_assets', '__return_false' );
    remove_action( 'wp_enqueue_scripts', 'wp_enqueue_global_styles' );
    remove_action( 'wp_footer', 'wp_enqueue_global_styles', 1 );
}
add_action( 'after_setup_theme', 'theme_remove_global_styles' );

Since version 3.0, I’ve also had a custom plugin that removes everything WordPress tries to shove into the header as well:

function cleanup_headers_init() {
	
	// Remove WP meta tags
	remove_action( 'wp_head', 'rsd_link' );
	remove_action( 'wp_head', 'wlwmanifest_link' );
	remove_action( 'wp_head', 'wp_generator' );

	// REST API discovery link (added WordPress 4.4).
	remove_action( 'wp_head', 'rest_output_link_wp_head' );

	// oEmbed discovery links and host JS (added WordPress 4.4).
	remove_action( 'wp_head', 'wp_oembed_add_discovery_links' );
	remove_action( 'wp_head', 'wp_oembed_add_host_js' );

	// Shortlink (added WordPress 3.0).
	remove_action( 'wp_head', 'wp_shortlink_wp_head' );

	// Emoji detection script and inline styles (added WordPress 4.2).
	remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
	remove_action( 'wp_print_styles', 'print_emoji_styles' );
	remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
	remove_action( 'admin_print_styles', 'print_emoji_styles' );
}
add_action( 'init', 'cleanup_headers_init' );

function cleanup_headers_remove_generator( $gen, $type = '' ) {
	return '';
}
add_filter( 'the_generator', 'cleanup_headers_remove_generator', 10, 2 );

Plugins

On the topic of plugins, my approach is usually “less is more” since I’ve found many plugins out there are rather bloated.

I run 3 custom plugins:

  • Cleanup headers: removes all of the headers that WordPress shoves into the head tag. See above for more details.
  • Meta headers: provides optional auto-generated keywords and description META tags as well as a pane to customize them on pages and posts. It also generates the OpenGraph META tags needed to customize what shows up in Messages, Slack, Facebook, and so on.
  • Sticky Unit Converter: provides auto-conversion of units between imperial and metric and saves that preference for users (e.g., 100 mi).

Beyond that, I run the following set of popular plugins:

  • Akismet Anti-spam: so my comment section isn’t filled with Ponzi schemes.
  • Code Block Pro: provides the beautiful formatting of code blocks you see above.
  • Docket Cache: object cache plugin. Page caching is the usual starting point, but that wasn’t a great match for me due to the per-user formatting of pages that happens based on cookies set. I could have excluded users with cookies set, but I still didn’t want to deal with 2 layers of caching in the end (see also: Two Hard Things). I still think this plugin has too much bundled into it (why are there WooCommerce optimizations in here?), but it’s the one endorsed by DreamHost on their platform.
  • Footnotes Made Easy: elegant footnotes on pages and posts.
  • Modern Image Formats: auto-converts PNGs/JPGs to WebP images to serve up smaller sizes. This was a pain to apply retroactively, so either use it for future pages/posts only or make sure it’s installed when you first spin up a WordPress site. Note that it messes up WordPress’s built-in Site Icon feature since it also converts those to AVIF/WebP images, which aren’t supported by most platforms yet; my theme has a site icon bundled in, so I just turned that feature off entirely.
  • Redirection: lets me change my URL scheme on a whim without breaking everything.

Building this with Claude

To start my redesign, I gave Claude Design a brief of what I wanted with links to example websites and had it draw up desktop and mobile wireframes. After a few iterations and providing it a color palette, I had it convert those to HTML designs that I could hand off to Claude Code. Claude Code took the first pass at the theme, but it stuck to the classic theme design and missed a bunch of optimizations.

Generally, I found Claude Code was good given specific tasks (e.g., take my single stylesheet and break it out into this 7-1 folder structure, show me how to add a special character via CSS), but it would really struggle to troubleshoot issues with the theme, especially with CSS, often pointing me in entirely the wrong direction. I went through a few different models up to the latest Opus and never really saw an improvement.

That said, Claude made building this way faster since it got all of the boilerplate/tedious work out of the way, allowing me to focus on higher-level architecture, design, and the harder issues with the theme. For more complex problems, I found it useful to challenge Claude by giving it a copy of Gemini’s response and have it evaluate its own response against that.

Now this theme just has to hold it together for another 16 years.

👨‍💻

  1. I imagine they will have to forever given the number of sites like mine that have a theme from a decade ago.↩︎
  2. In a decision I still can’t understand, WordPress will alter the markup for certain blocks entirely based on whether a theme has a theme.json file or not, so tread carefully if you’re adding one.↩︎
  3. Shoutout to Bill Erickson’s super useful post on hybrid themes as a starting point.↩︎
  4. Technically, 6-1 since I’m not using the themes folder as that would be a bit redundant given this is a WordPress theme.↩︎