WordPress overview

WordPress has a lot of directories which you might need. And a lot of functions to get those directories. I myself often doubt which function returns what. Ans if it has a trailing slash or not. so here is an overview. I’ve also put all these functions in this small plugin: URL-overview plugin

I’m assuming a few things:

  • The website is: https://janw.me/
  • WordPress is in a subdirectory: https://janw.me/wp/
  • The website root on the server is: /var/www/janw.me/www/
  • So wordpress is in: /var/www/janw.me/www/wp/
  • The theme is called ‘child-theme’ and it’s parent is ‘parent-theme’

The Url’s that bloginfo() usses:

bloginfo('url')                  // https://janw.me  use home_url('/')
bloginfo('wpurl')                // https://janw.me/wp use site_url('/')
bloginfo('stylesheet_directory') // https://janw.me/wp/wp-content/themes/child-theme
bloginfo('stylesheet_url')       // https://janw.me/wp/wp-content/themes/child-theme/style.css
bloginfo('template_directory')   // https://janw.me/wp/wp-content/themes/parent-theme
bloginfo('template_url')         // https://janw.me/wp/wp-content/themes/parent-theme
// Does not go the the style.css unlike the child theme counterpart

Constants:

WP_HOME         // https://janw.me
WP_SITEURL      // https://janw.me/wp
ABSPATH         // /var/www/janw.me/wp/
WP_CONTENT_DIR  // /var/www/janw.me/www/wp/wp-content
WP_CONTENT_URL  // https://janw.me/wp/wp-content
WP_PLUGIN_DIR   // /var/www/janw.me/wp/wp-content/plugins
WP_PLUGIN_URL   // https://janw.me/wp/wp-content/plugins
WPMU_PLUGIN_DIR // /var/www/janw.me/wp/wp-content/mu-plugins
WPMU_PLUGIN_URL // https://janw.me/wp/wp-content/mu-plugins
UPLOADS         // /var/www/janw.me/wp/wp-content/uploads
// UPLOADS is only used on multisite installs

Theme

get_template_directory()       //  /var/www/wordpress.dev/sandbox/content/themes/child-theme
get_template_directory_uri()   //  https://sandbox.wordpress.dev/content/themes/parent-theme
get_stylesheet_directory_uri() //  https://sandbox.wordpress.dev/content/themes/child-theme
get_stylesheet_uri()           //  https://sandbox.wordpress.dev/content/themes/child-theme/style.css
get_theme_root_uri()           //  https://sandbox.wordpress.dev/content/themes
get_theme_root()               //  /var/www/wordpress.dev/sandbox/content/themes
get_theme_roots()              //  /themes

Plugins

plugins_url()                //  https://sandbox.wordpress.dev/content/plugins
plugin_dir_url(  __FILE__ )  //  https://sandbox.wordpress.dev/content/plugins/plugin-folder/
plugin_dir_path(  __FILE__ ) //  /var/www/wordpress.dev/sandbox/content/plugins/plugin-folder/
plugin_basename(  __FILE__ ) //  plugin-folder/plugin-file.php

Uploads

$wp_upload_dir = wp_upload_dir() // use wp_upload_dir('2013/03'); to set it to march 2013
$wp_upload_dir['path']           //  /var/www/janw.me/www/wp/wp-content/uploads/2013/03
$wp_upload_dir['url']            //  https://janw.me/wp/wp-content/uploads/2013/03
$wp_upload_dir['subdir']         //  /2013/03
$wp_upload_dir['basedir']        //  /var/www/janw.me/www/wp/wp-content/uploads
$wp_upload_dir['baseurl']        //  https://janw.me/wp/wp-content/uploads
$wp_upload_dir['error']          //  should be FALSE or the error message

Multisite

network_admin_url()  //  https://janw.me/wp/wp-admin/
network_site_url()   //  https://janw.me/wp
network_home_url()   //  https://janw.me

Other functions

home_url()      // https://janw.me
site_url()      // https://janw.me/wp
admin_url()     // https://janw.me/wp/wp-admin/        including trailing slash
includes_url()  // https://janw.me/wp/wp-includes/  including trailing slash
content_url()   // https://janw.me/wp/wp-content
get_home_path() // /var/www/janw.me/public_html/  after `admin_init` only
get_rest_url()  // https://janw.me/wp-json/
home_url(add_query_arg(null, null)); // get the absolute current url including the query string

Get full current url

global $wp;
$current_url = add_query_arg( $wp->query_string, '', home_url( $wp->request ) );

Source
So this gets the full current url on the frontend.

Preview installable versions with apt

The output of "sudo apt list" for multiple packages.

The main thing is to just use sudo apt list PACKAGE_NAME
It will show the full package name, which should include the version number.

The syntax will differ, nginx will plainly show 1.18.0 but php is a less readable. But it’s 8.1 in this screenshot.

On this server both php and nginx are installed. In red I highlighted that nginx is installed. php does not have this tag.
That is (probably) because there is an update available.

For nginx there are multiple packages available, that can be show with the flag --all-versions
In this example only nginx has multiple packages available, with the same version number.
I currently have no clue how to read this.🤷


Create full site Zip Backup

The following command will create a full backup in a tar file.
The main reason I’m adding it here is because I want a exhaustive and growing list of excludes.

Run this above the public_html. This way the backup won’t be publicly available.

wp db dump dump.sql --path=./public_html
tar  --exclude="wp-content/aiowps_backups" --exclude="wp-content/backup" --exclude="wp-content/cache" --exclude="wp-content/updraft" --exclude="wp-content/wpvividbackups" --exclude="wp-content/uploads/jetbackup" -vczf public_html.tar.gz public_html/ dump.sql
rm dump.sql

Importing the Dump.

The unzip command:

tar -xf ./public_html.tar.gz -C ./
echo 'path: ./public_html' > wp-cli.yml

Now change the DB settings in the wp-config.

wp db reset --yes
wp db import dump.sql

Change the url.

wp search-replace --all-tables --report-changed-only //original.url.com //new.site.url.com

After testing the site works you can delete the import files.

rm ./public_html.tar.gz ./dump.sql

Staging site.

If this copy works as a staging site and should not be live (yet) I recommend using Restricted Site Access.
By default it will redirect everybody to the login screen. Under Settings > reading you can tweak a lot.

wp plugin install --activate restricted-site-access

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 wordpress.org.
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 wordpress.org 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 wordpress.org. 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 wordpress.org.

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

<?php
$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 );
		continue;
	}

	// Load the innerHTML as an object so it's safer to manipulate.
	$dom = new DOMDocument;
	$dom->loadHTML( $block['innerHTML'], LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD );

	/** @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.

<?php
$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(
	array(
		'name'  => __( 'grey', 'theme translation' ),
		'slug'  => 'grey',
		'color' => '#111111',
	),
	array(
		'name'  => __( 'red', 'theme translation' ),
		'slug'  => 'red',
		'color' => '#ee4035',
	),
	array(
		'name'  => __( 'orange', 'theme translation' ),
		'slug'  => 'orange',
		'color' => '#f37736',
	),
	array(
		'name'  => __( 'yellow', 'theme translation' ),
		'slug'  => 'yellow',
		'color' => '#fdf498',
	),
	array(
		'name'  => __( 'green', 'theme translation' ),
		'slug'  => 'green',
		'color' => '#7bc043',
	),
	array(
		'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(
		array(
			'name'  => __( 'grey', 'theme translation' ),
			'slug'  => 'grey',
			'color' => '#111111',
		),
		array(
			'name'  => __( 'red', 'theme translation' ),
			'slug'  => 'red',
			'color' => '#ee4035',
		),
		array(
			'name'  => __( 'orange', 'theme translation' ),
			'slug'  => 'orange',
			'color' => '#f37736',
		),
		array(
			'name'  => __( 'yellow', 'theme translation' ),
			'slug'  => 'yellow',
			'color' => '#fdf498',
		),
		array(
			'name'  => __( 'green', 'theme translation' ),
			'slug'  => 'green',
			'color' => '#7bc043',
		),
		array(
			'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 example.com A +short

Check AAAA record (ipv6)

dig example.com AAAA +short

Check CNAME

dig example.com CNAME +short

Check A, AAAA and CNAME all at once.

dig  example.com AAAA example.com CNAME example.com 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:

<?php

add_action( 'admin_menu', 'register_menu_item' );

function register_menu_item() {
	add_submenu_page(
		'options-general.php',
		__( 'Settings page title' ),
		__( 'Menu title' ),
		'manage_options',
		'page-slug',
		function () {
			require './templates/settings-page.php';
		}
	);
}

add_action( 'admin_init', 'register_settings' );

/**
 * Register settings to save.
 */
function register_settings() {
	register_setting(
		'fake-option',
		'fake-option',
		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 ) {
		return;
	}
	$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." ) );

		return;
	}
	$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">
            <tbody>
            <tr>
                <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>
            </tr>
            <tr>
                <th scope="row">More fields</th>
                <td></td>
            </tr>
            </tbody>
        </table>
		<?php submit_button( __( 'Add log' ) ); ?>
    </form>
</div>

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.

<?php
function register_settings() {
	register_setting(
		'fake-option-create',
		'fake-option-create',
		array( 'sanitize_callback' => 'save_post' )
	);
	register_setting(
		'fake-option-delete',
		'fake-option-delete',
		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' ); ?>
        HTML FORM HERE
		<?php submit_button( __( 'Add log' ) ); ?>
    </form>
    <form action="options.php" method="POST">
		<?php settings_fields( 'fake-option-delete' ); ?>
        LIST POSTS HERE WITH CHECKBOXES
		<?php submit_button( __( 'Delete logs' ) ); ?>
    </form>
</div>

Other tweaks.

?php_version=7.4&platform=wordpress&platform_version=5.6&software=free&software_version=15.6.2&days_active=30plus&user_language=en_US

  • 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.

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.

Sources: