Guide to upgrading old WordPress themes
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 user@server.com:~/mikebian.co ./
Use docker to whip up a mysql server:
version: '3.1'
services:
mysql:
image: mysql:8.0
container_name: mysql_container
environment:
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
ports:
- "3306:3306"
volumes:
- mysql-data:/var/lib/mysql
volumes:
mysql-data:
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 user@server.com:~/mikebian.co/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/ user@server.com:~/mikebian.co/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
not-prose
to all pre
tags
Adding 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) {
break;
}
$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:
- Clone your plugin. This makes it easy to submit upstream PRs
- Make changes in the plugin locally.
- 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/mikebian.co/wp-content/plugins/wp-githuber-md/
- 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:
- Write a script using the WP API
- Paste into the top-level directory
- Run with
wp eval-file ./the-file.php
- Boom!
A couple example scripts below (generated with the aid of ChatGPT) that helped me migrate my code blocks across the site.
code
tag language type
Add 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()) {
$query->the_post();
$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());
}
}
}
wp_reset_postdata();
}
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.
<?php
define('WP_USE_THEMES', false);
require('./wp-load.php');
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()) {
$posts_query->the_post();
$post_id = get_the_ID();
$content = get_the_content();
$markdown_detector->detect_code_languages($post_id, $content);
}
wp_reset_postdata();
}
}
detect_all_post_languages();
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:
<?php
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] . '">';
},
$post_content
);
}
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) {
wp_update_post(array(
'ID' => $post->ID,
'post_content' => $updated_content
));
}
$permalink = get_the_permalink($post);
WP_CLI::log("Updated post {$permalink}");
}
}
}
modify_code_tags_in_posts(false);
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