Get Checksum of a directory

There was a big-ish directory and I needed to compare the live copy to my local copy. I could’ve downloaded the live directory, but checksum was probably faster.

So in the terminal I opened the directory on both local and live via ssh.
Then I just simple ran:

find . -type f -exec md5sum {} \; | sort -k 2 | md5sum

And it just gave me the sumcheck.
Both directories where the same, otherwise I would’ve downloaded it and compared it in phpstorm.

(source of the command)

Github actions

GitHub actions allow you to automate processes when code get send to GitHub. For example: when a pull request is approved or when a single commit is pushed. There are a lot of events and the rabbit hole goes deep. If you can think of something to automate, it’s probably possible.
In the past I’ve worked a bit with Bitbucket pipelines which basically can do the same thing.
But lately I’ve been working solely with GitHub and it’s actions.

Here I will summarize my beginner experience with GitHub actions.

Importing actions

This June I released plugin Disable Full Site Editing. It’s codebase is hosted on GitHub. And actions deploy releases to
These are actions imported from 10up. I only configure these actions.

If you take a look at the deploy.yml file you will see the line uses: 10up/action-wordpress-plugin-deploy@stable
Here I import the action form 10up. The rest is just configuration. See the 10up repo for a good instruction.
Now when I create a new release It will automatically push the release to the svn.

It’s great. 10up maintains the bash script. And for me -as a user- I don’t have to know the workings of the script. I can rely on the action to do the work.
I also use a separate action for updating the plugin page on Also imported form 10up.
Also 10up has a couple of more actions for WordPress plugins. Check them out.

Limits of imported actions

Pre-made actions are great and I would prefer it over building something myself. But what if you need some else?
I developed a plugin which was only available via direct download on the company webpage and not via

So I needed a custom action. To keep it short it needed to create a plugin zip file and upload it to the correct server.

Official GitHub actions tutorial

I recommend starting with the official GitHub tutorial for actions. This tutorial impressed me. When starting it will create a repository and will guide you step by step inside this newly created repository. I highly recommend starting there.

Here is my instance of that repository. This functions as a cheat-sheet and playground for me regarding github actions.

Inside the main.yml I’ve listed all variables I find useful. Also I’ve listed a couple of tricks like importing an external action and running bash commands.
There are a couple of things I still haven’t done like functions. And I know there are more features I haven’t explored.

For now this is a good start for me and I’ll update the repo when I learn new tricks.

Change saved gutenberg blocks

When a post is saved the Gutenberg Blocks are rendered and it’s html is saved in the post_content in the database, most of the time. But what if you want to change the rendered html for all posts in every post.
Here we will explain how to change the html of saved Gutenberg blocks.

Add a class to all paragraph blocks

This use case is highly unlikely, but it is a simple one. So making it easier to explain.
So lets assume you would want to add a newer class to every paragraph on a single post on your site.

To start the block looks like this:

<!-- wp:paragraph -->
<p>To start the block looks like this:</p>
<!-- /wp:paragraph -->

With the added newer class it will look like this:

<!-- wp:paragraph {"className":"newer"} -->
<p class="newer">To start the block looks like this:</p>
<!-- /wp:paragraph -->

With the use case explained lets show the code to handle this.

changing the html of a block

The following code will change this html

$post                 = get_post( 898 ); // Get 1 post
$parsed_blocks        = parse_blocks( $post->post_content ); // parse all blocks in this post.
$updated_post_content = ''; // The updated post content will be inside here.

// Loop over the parsed blocks.
foreach ( $parsed_blocks as $block ) {
	if ( 'core/paragraph' !== $block['blockName'] ) {
		// only convert paragraphs, the rest we re-add untouched.
		$updated_post_content .= serialize_block( $block );

	// Load the innerHTML as an object so it's safer to manipulate.
	$dom = new DOMDocument;

	/** @var DOMElement $paragraph_node Get the <p> as a object */
	$paragraph_node = $dom->getElementsByTagName( 'p' )->item( 0 );
	// Get already set classes.
	$current_classes = $paragraph_node->getAttribute( 'class' );
	// Append `newer` class to the current list.
	$all_classes = trim( $current_classes . ' newer' );

	// Add the class attribute.
	$paragraph_node->setAttribute( 'class', $all_classes );
	$new_content = $dom->saveHTML();

	// Now put the new block together.
	$new_block = array(
		// We keep this the same.
		'blockName'    => 'core/paragraph',
		// also add the class as block attributes.
		'attrs'        => array( 'className' => $current_classes ),
		// I'm guessing this will come into play with group/columns, not sure.
		'innerBlocks'  => array(),
		// The actual content.
		'innerHTML'    => $new_content,
		// Like innerBlocks, I guess this will is used for groups/columns.
		'innerContent' => array( $new_content ),

	$updated_post_content .= serialize_block( $new_block );

// Update the post with the content.
wp_update_post( array( 'ID' => $post->ID, 'post_content' => $updated_post_content, ) );

Run this code once and it will update this one post, ID 898 in this case. Pretty much every line has an explaining comment. But let’s go through the main steps.

First we get the post we want and convert to content to an array of blocks.
We loop over these blocks one by one.
As we only are changing the paragraphs we check and re-add other block types.

The paragraph blocks we load as a DomDocument. We only need to add or edit one attribute. you might be able to do this with a str_replace but DomDocument is more flexible and it can handle edge cases with more safety.

After that we check for existing classes. And add our newer class to the existing ones.
Even if there are no existing ones that’s fine.
Once the class is added we re-render the html content of the block.

The last thing we do is re-create an array that is the definition of a block. And render is as a html block.

Finally after all blocks are looped over we update the post.
You now updated all the Paragraph Gutenberg blocks in the post.

Change multiple posts

Now we covered updating a single post, we can pretty much wrap this in a loop to cover all posts.

$all_post_types = get_posts( array(
	'post_type'   => array( 'post', 'page' ), // add any posttype you would want to check.
	'post_status' => 'any', // all except trash.
	'nopaging'    => true, // no limits, get all posts.
) );

foreach ( $all_post_types as $post ) {
	// insert the code from the sample above

Closing thoughts

Keep in mind the final code you only need to run it once.
Before doing this I recommend creating a test post with a bunch of the same blocks but with all kind of options/settings tweaked. That should give you a good test case.

And of course create a backup before converting all posts and do deep check when it’s done.

Add Color Pallets to Gutenberg child theme

How to customize the color pallets you can select in a gutenberg block in your theme. And the tweak if you are running a childtheme.

In Gutenberg you can customize the colors of the some blocks like the paragraph block (this is a paragraph block.) The theme can add some default colors to pick from or a user can pick his a custom color.

Define theme color pallets

In a theme add the following code to the functions.php:

add_theme_support( 'editor-color-palette', array(
		'name'  => __( 'grey', 'theme translation' ),
		'slug'  => 'grey',
		'color' => '#111111',
		'name'  => __( 'red', 'theme translation' ),
		'slug'  => 'red',
		'color' => '#ee4035',
		'name'  => __( 'orange', 'theme translation' ),
		'slug'  => 'orange',
		'color' => '#f37736',
		'name'  => __( 'yellow', 'theme translation' ),
		'slug'  => 'yellow',
		'color' => '#fdf498',
		'name'  => __( 'green', 'theme translation' ),
		'slug'  => 'green',
		'color' => '#7bc043',
		'name'  => __( 'blue', 'theme translation' ),
		'slug'  => 'blue',
		'color' => '#0392cf',
) );

Above is the example of the 6 colors that this theme uses. The name is used when hovering over the color pallet. the slug shouldn’t matter as long as they are different.
Finally the color is simple hexadecimal color notation.

This will work on normal theme’s. But not on child themes.

Define color pallets for child themes.

The following might differ a bit depending on the parent theme setup. But the trick is to add the colors after the parent does it. My theme is a child of twentysixteen.

The best way to overrule the parent should be to add the code as this:

function janwsixteen_after_setup() {
	// Adds support for editor color palette.
	add_theme_support( 'editor-color-palette', array(
			'name'  => __( 'grey', 'theme translation' ),
			'slug'  => 'grey',
			'color' => '#111111',
			'name'  => __( 'red', 'theme translation' ),
			'slug'  => 'red',
			'color' => '#ee4035',
			'name'  => __( 'orange', 'theme translation' ),
			'slug'  => 'orange',
			'color' => '#f37736',
			'name'  => __( 'yellow', 'theme translation' ),
			'slug'  => 'yellow',
			'color' => '#fdf498',
			'name'  => __( 'green', 'theme translation' ),
			'slug'  => 'green',
			'color' => '#7bc043',
			'name'  => __( 'blue', 'theme translation' ),
			'slug'  => 'blue',
			'color' => '#0392cf',
	) );
add_action( 'after_setup_theme', 'janwsixteen_after_setup', 100 );

Now this code will run during the after_setup_theme event at priority 100 (which is a late priority).
If this does not work I recommend setting replacing the action after_setup_theme to init.

Check DNS records in terminal

Currently I’m working on a big server migration. Where a lot of small sites get moved over. And a lot of the domain names are registered and managed at third parties.
So after each move I have to check a lot of DNS records to see if the move is final.

Usually I use the mxtoolbox which is great to check DNS records.
But I needed a more bulk option for this mass migration.

Enter the dig command

Dig commands

Check A record:

dig A +short

Check AAAA record (ipv6)

dig AAAA +short


dig CNAME +short

Check A, AAAA and CNAME all at once.

dig AAAA CNAME A +short

These commands return very compact ip’s or domainnames.
Which are easy to scan.

I’m not sure if these are cached.

WordPress Settings API save post

The WordPress Settings API (docs) is used to create settings pages and save settings in the options table. But I didn’t need a setting, I needed a settings page to create/update a post.

Of course I could have created my own custom settings page and not use the settings API. But the settings API does have a couple of advantages.

  • Nonces validation.
  • Still using a WordPress standard.
  • Saving and error notices by default and customizable.
  • Mix with regular options.

First off create a settings page like we are used to:


add_action( 'admin_menu', 'register_menu_item' );

function register_menu_item() {
		__( 'Settings page title' ),
		__( 'Menu title' ),
		function () {
			require './templates/settings-page.php';

add_action( 'admin_init', 'register_settings' );

 * Register settings to save.
function register_settings() {
		array( 'sanitize_callback' => 'save_post' )

 * This function will only be called when the "option" is valid to save.
 * Here we validate our own fields and create the post we want.
 * @param null|array $unused_fake_setting
function save_post( $unused_fake_setting ) {
	// Nonces are validated before we get here.

	static $first_run = true; // this function can be called twice, but we only need it once.
	if ( ! $first_run ) {
	$first_run = false; // Never run again, this request.

	if ( empty( $_POST['log_title'] ) ) {
		add_settings_error( 'fake-option', 'title-empty', __( "Log title can't be empty." ) );

	$title = wp_unslash( $_POST['log_title'] );
	$title = sanitize_text_field( $title );

	// validate other fields...
    // save the post.
	wp_insert_post( array(
		'post_title' => $title
		// add other attributes.
	) );

	// success message, yes the function name is confusing.
	add_settings_error( 'wpjm-ch-save', 'term-added', __( 'Worker successfully connected', 'success' ) );


And the templates/settings-page.php file:

<div class="wrap">
    <h1><?php esc_html_e( 'Settings page title' ); ?></h1>
    <form action="options.php" method="POST">
		<?php settings_fields( 'fake-option' ); ?>
        <h2><?php esc_html_e( 'Add log' ); ?></h2>

        <table class="form-table" role="presentation">
                <th scope="row"><label for="log_title"><?php esc_html_e( 'Log title' ); ?></label></th>
                <td><input name="log_title" id="log_title" type="url" class="regular-text" required/></td>
                <th scope="row">More fields</th>
		<?php submit_button( __( 'Add log' ) ); ?>

How it works

We basically add settings like normal, except we skip a few things
First off normally you would add a section using add_settings_section in the same function where we register the settings field(s). But as we don’t really render a settings field in the template, we don’t add it at all. The same goes for the do_settings_sections in the Template.

In the settings template we do have the regular settings_fields this makes sure all the nonce fields are printed. And the submit_button.

Finally the thing we do extra and where the magic happens.
In the sanitize_callback of the register_setting function we set the save_post function.
Normally this is for sanitizing the option you want to save. But in this case we are not saving a option, we are saving a post. In this function we validate all the data send by the form, sanitize the data and save a new post.

And when the data is validated and saved we send a success notice with the add_settings_error function. Yes, we send a success notice with the **_error function. It can be done with the final argument, by setting the type to ‘success’.

Handling multiple custom options.

In the project that sparked this post I need to create a post, but also list those posts with a delete button.
What I found useful was creating a second register_setting, put them in two separate <form> tags on the template and let one handle the creating and the other the deleting.

function register_settings() {
		array( 'sanitize_callback' => 'save_post' )
		array( 'sanitize_callback' => 'delete_post' )
function save_post()  {
    // handles saveing/creating.
function delete_post() {
    // handles deleting.

<div class="wrap">
    <h1><?php esc_html_e( 'Settings page title' ); ?></h1>
    <form action="options.php" method="POST">
		<?php settings_fields( 'fake-option-create' ); ?>
		<?php submit_button( __( 'Add log' ) ); ?>
    <form action="options.php" method="POST">
		<?php settings_fields( 'fake-option-delete' ); ?>
		<?php submit_button( __( 'Delete logs' ) ); ?>

Other tweaks.


  • Create, delete and updates posts (and post meta)
  • The same for user(meta and tax(meta)
  • External API calls.
  • Mix in regular use of the settings API.
  • World peace.

Mount a USB drive on Raspberry Pi

SD cards can hold a lot of data these days. I bought a 64GB micro SD for €15 the other day. While that is more then enough for the OS and most work, it’s not enough for my backups.

SD cards can hold a lot of data these days. I bought a 64GB micro SD for €15 the other day. While that is more then enough for the OS and most work, it’s not enough for my backups.
So in this guide we are mounting a 4TB external hard drive with a USB cable.

Sidenote, If you have a new drive I recommend to format it as ext4, that is not part of this tutorial. It’s easier to do on a your main Latop/Computer with an interface.

Boot up your Pi, login and plug in your drive.

Find the USB drive before mounting

First we run the command: sudo blkid
The output should look like this.

List of drive
List of available drives

This is a list of all avialable drives. The first , the /dev/mmcblk0 are part of the Rasberry itself. External drives should be like /dev/sda or similar. In my case it’s also easy to identify by the label.
The important thing to note is the UUID. Copy that, we will need it in the next steps.

Mounting the drive

Before we mount the drive it’s important to know where we are going to mount the drive. Traditional places are /media/* and /mnt/* So we will create a directory inside there:

sudo mkdir /media/4TB

Finally we simply mount the directory.

sudo mount /dev/sda /media/4TB

And that’s it.

The result before and after mounting a drive
Before mounting the folder is empty, after you see the folders I already had on the drive.

Mount a drive on startup

The method above is ease, but has one major downside. If the Pi ever is rebooted you will have to manually mount again.
Try it out now: sudo reboot now and log in again, and check your folder, it should be empty.

Before we start we need the UUID from the drive. You can find it in with this command: sudo blkid
Now open the fstab file where all mount points are stored.

sudo nano /etc/fstab

Inside add the following line, replace the UUID and optinal the path where you want to mount your drive. I’m mounting it at /media/4TB
I’m also amusing the drive is formatted ext4, if not adjust that accordingly

UUID=12272d4a-c58e-4b32-a908-ed8bf737db54    /media/4TB   ext4    defaults,errors=remount-ro 0       1

To test that is works you can mount it with: sudo mount -a
If that works reboot the system and check again: sudo reboot now

For more details about the fstab and it’s options check this this fstab page

Install Let’s Encrypt SSL on Raspberry Pi

I love Let’s Encrypt. It’s free SSL, it’s safer because of the auto-renewal and it’s so easy to setup. No more emailing around validating company name and all that hassle.

I love Let’s Encrypt. It’s free SSL, it’s safer because of the auto-renewal and it’s so easy to setup. No more emailing around validating company name and all that hassle.
Before you start you do need to have opened your router to the outside world.

The installation on server level has also got a lot easier in the past years. The following will work for both Raspbian and Ubuntu.
For other systems check the official site, just select the correct OS and webserver.

Install Certbot

Let’s Encrypt will work through a program called Certbot. We will install Certbot and that will get the free SSL certificates from Let’s Encrypt.

To install run these 2 commands, these might take a while:

sudo snap install core; sudo snap refresh core
sudo snap install --classic certbot

Once this is done we can start getting the SSL-certificates. Run:

sudo certbot --nginx

Since it’s the first time running it will ask for email and some other stuff.

The first setup of Certbot
The first setup of certbot

For the next step you can select specific domains, or all.

The first setup of Certbot
The first setup of certbot

It should create and set all certificates correctly. If in the future you add a new domain just rerun the command sudo certbot --nginx and select the new domains.

Auto renewal

Let’s Encrypt certificates are valid for 90 days, this is for security reasons. This does mean they need to get renewed periodically.
By default Certbot should have installed auto renewal. But we are going to check it with the command: systemctl list-timers --all

Out put of command systemctl list-timers --all
Checking Timers for certbot auto renewal

There should be some output about snap.certbot.renew.timer.
If not we are going to setup the renewal ourself.

The command to renew the certificates is sudo certbot renew
You can also run this command by itself to test it out.

As it is run by root we will also add it to the crontab of the root user.

sudo crontab -e

Inside we add the following command.

0 0 * * 1 certbot renew

Here we set it to run every Monday at 0:00 (so Sunday to Monday night)
That’s more then enough, you can run it more if you want, I would not run it less.

Test your SSL connection.

Finally go to and test your site. Behind the scenes Certbot also adds a bunch of security settings which will help get that A+.

SSLlabs results for
The results for on

Installing WordPress Coding standards Globally

When possible I like to code using the WordPress coding Standards. I always set my PHPstorm coding style to WordPress. And when I start a new plugin I implement the PHPCodeSniffer rules for WordPress.
In the most ideal scenario you can include these in your composer settings, but that’s not always possible so it’s handy to have them installed globally.

To install run these 2 commands:

composer global require --dev squizlabs/php_codesniffer wp-coding-standards/wpcs phpcompatibility/phpcompatibility-wp dealerdirect/phpcodesniffer-composer-installer wptrt/wpthemereview
composer global update --dev --with-dependencies

Now all you need is installed. In your global composer directory. You can get that running: composer config home -g
By default is should return `~/.config/composer`

To test the phpcs with the global dir I run: ~/.config/composer/vendor/bin/phpcs -i
The output should look like this:

Result of command is "The installed coding standards are PSR12, PEAR, PSR2, PSR1, Squiz, MySource, Zend, PHPCompatibility, PHPCompatibilityParagonieRandomCompat, PHPCompatibilityParagonieSodiumCompat, PHPCompatibilityWP, WordPress-Docs, WordPress, WordPress-Extra, WordPress-Core and WPThemeReview"
Installed phpcs rulesets

Running this whole command will get annoying and will cluter up the terminal. So we will put the bin directory in the global $PATH
The bin directory we already got. (but we can also get using composer global config bin-dir --absolute)

To your ~/.bashrc add:

export PATH="$HOME/.config/composer/vendor/bin:$PATH"

Open a new terminal and test the command phpcs -i it should now work globally and is ready to run on individual projects.


Install Ubuntu on Raspberry Pi

This guide will only cover the Ubuntu server part, so not the desktop.At the end of this guide you will have a headless Raspberry Pi running Ubuntu that you can login with ssh.

Let me state a few things before we start.
This guide will only cover the Ubuntu server part, so not the desktop. There are other guides that do cover that.
At the end you will have a headless Raspberry Pi running Ubuntu that you can login with ssh.

Download and Flash

From the official downloads page download the Raspberry Pi Imager. Install and run it. Select the latest LTS version of Ubuntu, select 64 bit for the Pi 3 or Pi 4.
You could install the latest version but I like that the LTS versions are supported for 5 years.

The main screen of the Raspberry Pi Imager application
Raspberry Pi imager
Selecting Ubuntu using the Raspberry Pi Imager
Selecting Ubuntu using the Raspberry Pi Imager

After the SD card is done

When the writing of the SD card is done. Create a folder named ssh in the root of the system-boot partition.

If you need wifi I suggest this guide, I don’t use wifi on my Pi’s so I haven’t tried.

Then plug in the SD card and powerup. Do wait 2~5minutes before actually trying. Ubuntu has to setup stuff for the first time. If after 20max you still can’t find an IP. The flashing of the SD card has gone wrong and you’ll have to restart.

Finding the IP-address

First we need to get the IP-address that the Pi is running on. There are multiple ways to find the IP, you could login to your router and check for the device. It’s hostname is probably “ubuntu.home“, “ubuntu.local” or plain “ubuntu”. Usually the it will start with 192.168.x.x

There is an official guide for finding the IP-address. It’s one of the more annoying things when you are new. I myself use nmap

First login in Ubuntu

Once you do have the IP-address login with the command ssh ubuntu@IP-HERE

Loggin in on Ubuntu for the first time
Login in on Ubuntu for the first time

The password is ubuntu
You will be asked to replace the default password with a new one. After changing it login again.

Default login for Ubuntu on Raspberry Pi
Default login for Ubuntu on Raspberry Pi

Now we can login we will change some things, some are optional.


For complete updates run: sudo apt update && sudo apt upgrade -y
This might take a while. When this is done for the first time you might have to reboot, I recommend rebooting anyway: sudo reboot now


The default hostname is ubuntu, that might be fine but I’d rather give it a name that is more descriptive of it’s purpuse. Check the current hostname with the command hostname it should be ubuntu

For my example I will update the hostname to raspberry-webserver
Run the following command with the name you want.
sudo hostnamectl set-hostname raspberry-webserver

Then Reboot: sudo reboot now

Changing the hostname
Changing hostname

Static IP-address

By default the Pi gets a dynamic IP, the main benefit of a static IP is that it never changes. This might depend on your router.
With this static IP I know I can always login with the same IP.

Before we start a few notes. This part is always a bit annoying.

  • I followed 2 guides for this section, check them for more information.
    itzgeek & serverlab
  • These example only works when an ethernet cable is attached, not on wifi.
  • If you can’t login after applying the changes pull the plug and stick the SD card in your computer and delete /etc/netplan/01-static-ip.yaml it can be found in the writeable partition.
  • Depending on your router the 3rd number of the IP might differ, so it might be it should be the same as the 3rd part of what you use to login originally.

Set the IP

In this example I will set the static IP to
First we need the default IP of the router. We need it for the gateway. Run ip route show

Running ip route show
Default ip here is

This output shows “default via” That’s the IP we need.

We will create an extra netplan config file: sudo nano /etc/netplan/01-static-ip.yaml And match this content. Make sure you replace the IP with your own.
This is a yaml file so the number of spaces/tabs matters.

  version: 2
  renderer: networkd
      dhcp4: no

Next we reload netplan to apply the configuration: sudo netplan generate && sudo netplan apply This will likely freeze your terminal. Close it and login in a new terminal tab.

Once you can login with the new IP it’s done.