Skip to content

Guide to upgrading old WordPress themes

Tags: docker, php, wordpress • Categories: Learning

Table of Contents

It’s been a very long time since I’ve updated my WordPress theme on this blog. Probably five years? Could be longer.

It finally broke via the latest WordPress update. I’ve been looking for a good excuse to update the site, learn tailwindcss, add SSL, and fix a couple of things on the site. Here are my notes from updating the blog to use a custom sage theme. It wasn’t as painful as I expected!

Local WordPress Development

Rsync your blog contents from remote server

rsync -avz -e ssh ./

Use docker to whip up a mysql server:

version: '3.1'

    image: mysql:8.0
    container_name: mysql_container
      MYSQL_ROOT_PASSWORD: my-secret-pw   # Change this to your desired root password
      MYSQL_DATABASE: testdb              # Optional: Creates a database named "testdb"
      MYSQL_USER: user                    # Optional: Creates a user "user" with password "password"
      MYSQL_PASSWORD: password
      - "3306:3306"
      - mysql-data:/var/lib/mysql


Now you can import your mySQL dump:

docker exec -i blog_container mysql -u root -ppassword < ~/Downloads/your_sql_dump.sql

Now install wp-cli and update the DB config

brew install wp-cli

wp config set DB_USER root
wp config set DB_PASSWORD password
wp config set DB_HOST localhost

Now run a db check:

wp db check

If you run into weird permission errors, you may need to adjust mysql host access permissions:

docker compose exec mysql mysql -u root -ppassword -e "GRANT ALL PRIVILEGES ON database_name.* TO 'user'@'%'; FLUSH PRIVILEGES;"

Alright, now we should be able to view the site locally!

wp server

Using Roots / Sage

First install the PHP package manager:

brew install composer

Now create the new theme:

cd wp-content/themes

composer create-project roots/sage bianco
composer require roots/acorn

cd bianco
yarn install
yarn build

It bothered me we needed to install acorn, but I can’t be fighting with PHP.

Now we can activate the new theme and refresh the page:

wp theme activate bianco

Not seeing new content? Stop caching, or disable other prod-related plugins:

wp plugin deactivate wp-fastest-cache

"Blade" is a templating language that PHP uses. Here’s how the template structure works:

  • views/ contains the top-level templates used
  • Any changes you make to tailwindo.config.js require a full restart of the dev server.
  • There is a set of WP CSS things that will conflict with tailwind defaults (esp layout-related stuff that messes with flex)

In bud.config.js you can change setProxyUrl to localhost:8080 to use the wp server. I ran into issues using the proxy server with wp server.

Now, go off and make changes to your theme. Then build it for production:

rm -rf vendor
composer install --no-dev

rm -rf public
yarn build

The rm is probably unnecessary, but I don’t understand this world too much, so I wanted to clean things up before running a production build.

Now we can rsync our changes back to production (ah, the beauty of dead-simple super-dangerous php deployment):

rsync -avz -e ssh ./wp-content/themes/bianco --exclude={"node_modules","yarn*","./composer*","TODO",".vscode",".editorconfig",".gitignore",".DS_Store"}

If you need to sync local plugin changes (for a single plugin):

rsync -avz -e ssh ./wp-content/plugins/wp-githuber-md/

The trailing / is important in all of these rsync commands.

Having trouble running wp on your remote server? wp --info to see what’s going on. Wrong php version being used?

export WP_CLI_PHP=$(which php-8.2)

At this point you should be ready to activate the new theme and do WordPress configuration. Remember to backup your SQL DB.

Note that some of these commands may be specific to my use of Dreamhost (I don’t love dreamhost, but it’s been fine, and I’ve had it for so long it’s not worth it to change).

WordPress Filters

Some filters I found useful when setting up my theme

Adding not-prose to all pre tags

I’m using tailwind + typography plugin with the excellent githuber-md plugin. The prose pre tag formatting conflicts with the highlight.js formatting that githuber-md adds. This filter adds the not-prose tag to all <pre> tags to eliminate this conflict:

add_filter('the_content', function ($content) {
    // Add 'not-prose' class to <pre> tags that already have a class attribute
    $content = preg_replace('/<pre(.*?)class="(.*?)"/', '<pre$1class="$2 not-prose"', $content);

    // Add 'not-prose' class to <pre> tags that don't have a class attribute
    $content = preg_replace('/<pre((?!class=).)*?>/', '<pre class="not-prose"$1>', $content);

    return $content;

Remove the annoying [...]

Remove the [...] excerpt continuation text. This always bothered me, I just want a standard ...:

add_filter('excerpt_more', function () {
    return "...";

Excerpt by sentence, not character

I wanted my excerpts truncated by sentence, not by character. Here’s what worked for me.

function truncateBySentence($content, $limit) {
    if (strlen($content) <= $limit) return $content;

    $sentences = preg_split('/(?<=[.!?])\s+/', $content);
    $result = "";

    foreach ($sentences as $sentence) {
        if (strlen($result . $sentence) > $limit) {
        $result .= $sentence . " ";

    return rtrim($result) . "..";

// cut off the excerpt at the end of a sentence, not randomly
add_filter('get_the_excerpt', function($excerpt, $post) {
    // remove all <pre> blocks completely, we don't want code in our excerpts (this may be specific to my blog content)
    $content = preg_replace('/<pre.*?>.*?<\/pre>/si', '', $post->post_content);

    // remove all HTML so we can work with plain old text when generating excerpts
    $content = strip_tags($content);

    $target_length = 750;
    $content = truncateBySentence($content, $target_length);

    // remove all newlines to avoid line breaks in the excerpt
    $content = preg_replace('/\s+/', ' ', $content);

    return $content;
}, 1, 2);

Fixing WordPress Plugins

The plugin ecosystem in wordpress is awesome, but it’s wordpress-style php, so things get broken easily. Here was my process for diving in and fixing a couple plugin issues:

  1. Clone your plugin. This makes it easy to submit upstream PRs
  2. Make changes in the plugin locally.
  3. Sync changes to your local repo using rsync. rsync -av --exclude-from=.gitignore --exclude=.DS_Store --exclude=.git-custom-branch --exclude=.git --exclude=.github ./ ~/Projects/
  4. Then you can easily submit PRs for various plugin changes

One-off WordPress Scripts

wp cli has a nice utility for running one-off scripts with the wordpress environment installed. ChatGPT is good at understanding wordpress (here’s how the script below was generated).

Here’s you need to do:

  1. Write a script using the WP API
  2. Paste into the top-level directory
  3. Run with wp eval-file ./the-file.php
  4. Boom!

A couple example scripts below (generated with the aid of ChatGPT) that helped me migrate my code blocks across the site.

Add code tag language type

Helpful with the wp-githuber-md plugin to make sure all code blocks are rendered consistently.

function update_code_tags($dry_run = true) {
    // Step 1: Query all posts
    $args = array(
        'post_type'      => 'post',
        'posts_per_page' => -1, // Get all posts
        'post_status'    => 'publish',
    $query = new WP_Query($args);

    if ($query->have_posts()) {
        while ($query->have_posts()) {
            $post_id = get_the_ID();
            $post_content = get_the_content();

            // Step 2: Apply regex replace
            $updated_content = preg_replace('/<pre([^>]+)?><code((?!class=).)*?>/', '<pre$1><code class="language-plaintext"$2>', $post_content);

            // Step 3: Check if content is updated, log URL and save changes
            if ($post_content !== $updated_content) {
                if (!$dry_run) {
                    wp_update_post(array('ID' => $post_id, 'post_content' => $updated_content));
                error_log("Updated post: " . get_the_permalink());
            } else {
                error_log("No changes: " . get_the_permalink());

Detect code language

Following up on the above script, this one runs the wp-githuber-md detection script to update post metadata to ensure the right highlighting code is loaded on-page.

define('WP_USE_THEMES', false);

function detect_all_post_languages() {
    // Instantiate the class
    $markdown_detector = new Githuber\Controller\Markdown();

    // Query all posts
    $args = array(
        'post_type' => 'post',
        'post_status' => 'publish',
        'posts_per_page' => -1
    $posts_query = new WP_Query($args);

    // Loop through all posts and apply the detect_code_languages function
    if ($posts_query->have_posts()) {
        while ($posts_query->have_posts()) {
            $post_id = get_the_ID();
            $content = get_the_content();

            $markdown_detector->detect_code_languages($post_id, $content);


Add language prefix

In older code highlighters, the language- prefix was not required or standardized. Here’s a script to add this to past posts:

function update_code_tags_class($post_content)
    return preg_replace_callback(
        '/<code class="((?!language-).+)">/',
        function ($matches) {
            WP_CLI::log("Found code tag with class {$matches[1]}");
            return '<code class="language-' . $matches[1] . '">';

function modify_code_tags_in_posts($dry_run = true)
    $args = array(
        'post_type' => 'post',
        'post_status' => 'publish',
        'posts_per_page' => -1
    $posts = get_posts($args);
    foreach ($posts as $post) {
        $updated_content = update_code_tags_class($post->post_content);
        if ($updated_content !== $post->post_content) {
            if (!$dry_run) {
                    'ID' => $post->ID,
                    'post_content' => $updated_content

            $permalink = get_the_permalink($post);
            WP_CLI::log("Updated post {$permalink}");


WordPress Plugins

Here’s the list of wordpress plugins I’ve found useful:

  • add-anchor-links
  • broken-link-checker
  • classic-editor
  • disqus-comment-system
  • dreamhost-panel-login
  • enable-media-replace
  • jetpack-widget-visibility
  • wp-retina-2x
  • public-post-preview
  • public-post-preview-configurator
  • really-simple-ssl
  • redirection
  • google-site-kit
  • sumome
  • updraftplus
  • wp-external-links
  • wp-fastest-cache
  • wp-githuber-md
  • wp-to-buffer
  • wordpress-seo