Wordpress

Install and maintain

Composer

"extra": {
	"webroot-dir": "www",
	"webroot-package": "wordpress",
	"installer-paths": {
		"app/plugins/{$name}/": ["type:wordpress-plugin"],
		"app/mu-plugins/{$name}/": ["type:wordpress-muplugin"],
		"app/themes/{$name}/": ["type:wordpress-theme"]
	}
}
<?php
/** Absolute path to the WordPress directory. */
if ( !defined('ABSPATH') )
	define('ABSPATH', dirname(__FILE__) . '/wordpress/');

/** Absolute path to the WordPress wp-content directory, which holds your themes, plugins, and uploads */
//define( 'WP_CONTENT_DIR', dirname(__FILE__) . '/wp-content' );
?>
<?php
include_once './vendor/autoload.php';
require( './wordpress/wp-blog-header.php' );
?>

Docker

Config

<?php
/*
Automatic Url + Content Dir/Url Detection for Wordpress
Based on https://gist.github.com/CMCDragonkai/7578784#gistcomment-1237365 and https://stackoverflow.com/questions/1175096/how-to-find-out-if-youre-using-https-without-serverhttps
*/
// Use realpath to resolve symlinks (like /homez.xxx/ to /home/)
$document_root = rtrim(str_replace(array('/', '\\'), '/', realpath($_SERVER['DOCUMENT_ROOT'])), '/');

$root_dir = str_replace(array('/', '\\'), '/', __DIR__);
$wp_dir = str_replace(array('/', '\\'), '/', rtrim(ABSPATH, '/'));
$wp_content_dir = str_replace(array('/', '\\'), '/', WP_CONTENT_DIR);

$root_url = substr_replace($root_dir, '', stripos($root_dir, $document_root), strlen($document_root));
$wp_url = substr_replace($wp_dir, '', stripos($wp_dir, $document_root), strlen($document_root));
$wp_content_url = substr_replace($wp_content_dir, '', stripos($wp_content_dir, $document_root), strlen($document_root));

$scheme = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off' || isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == 443 || isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https') ? 'https://' : 'http://';
$host = rtrim($_SERVER['SERVER_NAME'], '/');
$port = (isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] != '80' && $_SERVER['SERVER_PORT'] != '443') ? ':' . $_SERVER['SERVER_PORT'] : '';

$root_url = $scheme . $host . $port . $root_url;
$wp_url = $scheme . $host . $port . $wp_url;
$wp_content_url = $scheme . $host . $port . $wp_content_url;

define('WP_HOME', $root_url); //url to index.php
define('WP_SITEURL', $wp_url); //url to wordpress installation
define('WP_CONTENT_URL', $wp_content_url); //wp-content url
?>

Update site URL

SET @current = 'http://some.example.com';
SET @next = 'https://www.example.com';
UPDATE wp_options SET option_value = replace(option_value, @curent, @next) WHERE option_name = 'home' OR option_name = 'siteurl';
UPDATE wp_posts SET post_content = replace(post_content, @curent, @next);
UPDATE wp_postmeta SET meta_value = replace(meta_value,@curent,@next);
UPDATE wp_usermeta SET meta_value = replace(meta_value, @curent,@next);
UPDATE wp_links SET link_url = replace(link_url, @curent,@next);
UPDATE wp_comments SET comment_content = replace(comment_content , @curent,@next);
# For images inside posts
UPDATE wp_posts SET post_content = replace(post_content, @curent, @next);
# For images linked in old link manager
UPDATE wp_links SET link_image = replace(link_image, @curent, @next);
# For images linked as attachments
UPDATE wp_posts SET guid = replace(guid, @curent, @next);
# Serialized data via serialize() or json_encode() aren't touched, see https://deliciousbrains.com/wp-migrate-db-pro/doc/serialized-data/

Plugins

Rest API

Themes

Simple/cleared theme: BlankSlate — Free WordPress Themes

<?php
remove_action('wp_head', 'rsd_link');
remove_action('wp_head', 'wlwmanifest_link');
remove_action('wp_head', 'wp_shortlink_wp_head');
remove_action('wp_head', 'wp_generator');
remove_action('wp_head', array( $sitepress, 'meta_generator_tag', 20 ) );
add_filter('xmlrpc_enabled', '__return_false');
add_filter('json_enabled', '__return_false');
add_filter('json_jsonp_enabled', '__return_false');
?>

Block theme

Aka theme.json

Template hierarchy

Theme development

Aka navigation menu

<?php
// .sub-menu
add_filter( 'nav_menu_submenu_css_class', function ( $classes, $args, $depth ) {
	if($args->theme_location == 'primary') $classes = array($depth == 0 ? 'ns-main-nav-subsections' : 'ns-main-nav-items');
	return $classes;
}, 10, 3);
// .menu-item > a
add_filter( 'nav_menu_link_attributes', function ( $atts, $item, $args, $depth ) {
	if($args->theme_location == 'primary') $atts['class'] = $depth == 0 ? 'ns-main-nav-section-link' : 'ns-main-nav-link';
	return $atts;
}, 10, 4);
// .menu-item
add_filter( 'nav_menu_css_class', function ( $classes, $item, $args, $depth ) {
	if($args->theme_location == 'primary'){
		switch ($depth){
			case 0:
				$class = 'ns-main-nav-section';
				break;
			case 1:
				$class = 'ns-main-nav-subsection';
				break;
			default:
				$class = 'ns-main-nav-item';
				break;
		}
		$classes = array($class);
	}
	return $classes;
}, 10, 4);

// Add wrapper to .sub-menu
//class Custom_Walker_Nav_Menu extends Walker_Nav_Menu {
//	public function start_lvl( &$output, $depth = 0, $args = null ) {
//		if($depth == 0) $output .= '<div class="mcp-main-nav-subsections-wrapper">';
//		parent::start_lvl($output, $depth, $args);
//	}
//	public function end_lvl( &$output, $depth = 0, $args = null ) {
//		parent::end_lvl($output, $depth, $args);
//		if($depth == 0) $output .= '</div>';
//	}
//}

// Render the nav menu
wp_nav_menu(
	array(
		'container'  => '',
		'menu_class'  => 'ns-main-nav-sections',
		'items_wrap' => '<ul id="%1$s" class="%2$s">%3$s</ul>',
		'theme_location' => 'primary',
//		'walker' => new Custom_Walker_Nav_Menu(),
	)
);
?>

Performance and optimisation

Use OPcache

Tools:

Plugins:

SAVEQUERIES should be false

Custom fields

Fields starts with _ are not visible defaut meta box for custom field

Gutenberg editor plugins can be written with JSX/ES6 or ES5 (without compilation)

Validation / sanitization / authorization:

Some:

See also:

Custom post types

Custom plugins

mu-plugins aka multi user plugins, must-use plugins

Taxonomy

Aka categories, tags

Rewrite

Regenerate thumbnails

Responsive images

Update URL and media filename

Backup and migration

AJAX

  • http://code.tutsplus.com/articles/getting-started-with-ajax-wordpress-pagination--wp-23099

  • http://codex.wordpress.org/AJAX_in_Plugins

  • http://www.smashingmagazine.com/2011/10/18/how-to-use-ajax-in-wordpress/

  • https://wordpress.org/plugins/rest-api/

Captcha

  • http://wordpress.org/plugins/really-simple-captcha/

Form

User feedback

  • http://wordpress.org/plugins/get-satisfaction-for-wordpress/

  • http://wordpress.org/plugins/user-voice/

  • http://www.onfry.com/projects/voteitup/

  • https://getsatisfaction.com/getsatisfaction/topics/pulling_back_support_for_wordpress_integration

  • https://developer.uservoice.com/docs/api/getting-started/

  • http://feedback.uservoice.com/forums/1-general-feedback

  • http://wordpress.org/plugins/uservoice-idea-list-widget/

  • http://feedback.uservoice.com/knowledgebase/articles/56243-use-a-link-custom-trigger-to-open-the-uservoice

  • http://feedback.uservoice.com/knowledgebase/articles/276635-use-just-smartvote-the-contact-form-or-satisfacti

  • GetSatisfaction

  • Zendesk

  • Desk

Job manager

  • http://wordpress.org/plugins/job-manager/

  • http://wordpress.org/plugins/wp-job-manager/

  • http://premium.wpmudev.org/blog/6-wordpress-job-board-solutions/

Social integration Twitter / Facebook

  • http://wordpress.org/plugins/facebook/

  • https://wordpress.org/plugins/twitter/

  • http://wordpress.org/plugins/recent-facebook-posts/

  • http://wordpress.org/plugins/custom-facebook-feed/

  • http://wordpress.org/plugins/fb-wallpost-widget/screenshots/

  • FB as RSS

Multilingual

Note: Has a performance impact

Localization

Aka translation

Use full locale name in .mo filenames For mu-plugins (see load_muplugin_textdomain()):

<?php
// In wp-content/mu-plugins/<pluginslug>/<pluginslug>.php (with a "loader" `wp-content/mu-plugins/<somename>.php`: `require WPMU_PLUGIN_DIR . '/<pluginslug>/<pluginslug>.php';`)
add_action('muplugins_loaded', fn() => load_muplugin_textdomain('textdomain', dirname(plugin_basename(__FILE__)) . '/languages'));
// This will try to load `<WP_LANG_DIR>/plugins/<textdomain>-<locale>.mo` else `wp-content/mu-plugins/<pluginslug>/languages/<textdomain>-<locale>.mo`
// Where `WP_LANG_DIR` is usually `wp-content/languages`
?>

For plugins (see load_plugin_textdomain()):

<?php
// In wp-content/plugins/<pluginslug>/<pluginslug>.php
add_action('init', fn() => load_plugin_textdomain('textdomain', false, dirname(plugin_basename(__FILE__)) . '/languages'));
// This will try to load `<WP_LANG_DIR>/plugins/<textdomain>-<locale>.mo` else `wp-content/plugins/<pluginslug>/languages/<textdomain>-<locale>.mo`
// Where `WP_LANG_DIR` is usually `wp-content/languages`
?>

For theme (see load_theme_textdomain()):

<?php
// In wp-content/themes/<themeslug>/functions.php
add_action('after_setup_theme', fn() => load_theme_textdomain('textdomain', get_template_directory() . '/languages'));
// This will try to load `<WP_LANG_DIR>/themes/<textdomain>-<locale>.mo` else `wp-content/themes/<themeslug>/languages/<locale>.mo`
// Where `WP_LANG_DIR` is usually `wp-content/languages`
?>

See also _get_plugin_data_markup_translate(), for a plugin with meta comment Domain Path: /languages (default to empty) and Text Domain: text-domain (require, or no text domain will be (auto-)loaded) will load wp-content/plugin/<pluginslug><domainpath>/<textdomain>-<locale>.mo.

To debug which .mo files are loaded:

<?php
$debuginfo_textdomain_mofiles = [];
add_filter('load_textdomain_mofile', function ($mofile, $domain) {
	global $debug_textdomain_mofiles;
	$is_mofile_readable = is_readable($mofile);
	$mo = new MO();
	$e = new Exception();
	$debug_textdomain_mofiles[] = array(
		'filename' => $mofile,
		'domain' => $domain,
		'readable' => $is_mofile_readable,
		'trace' => $e->getTraceAsString(),
		'importable' => $is_mofile_readable && $mo->import_from_file($mofile)
	);
	return $mofile;
}, 10, 2);
// Then later in page:
//var_dump($debuginfo_textdomain_mofiles);
?>
<?php
printf(
	/* translators: %s: Price amount. */
	__('Price: %s', 'my-text-domain'),
	get_product_price()
);
?>

Plurals

<?php
/* translators: %d: number of people. */
printf( _n( '%s person', '%s people', $count, 'text-domain' ), number_format_i18n( $count ) );

/* translators: 1: WordPress version number, 2: number of bugs. */
printf( _n( '<strong>Version %1$s</strong> addressed %2$d bug.', '<strong>Version %1$s</strong> addressed %2$d bugs.', $bugs_count, 'text-domain' ), $wp_version, $bugs_count );
?>

SVG

// In functions.php
function add_svg_to_upload_mimes( $upload_mimes ) {
	$upload_mimes['svg'] = 'image/svg+xml';
	$upload_mimes['svgz'] = 'image/svg+xml';
	return $upload_mimes;
}
add_filter( 'upload_mimes', 'add_svg_to_upload_mimes', 10, 1 );

Security

See Wordpress

File permissions for automatic update

Apache use a different group than FTP users. It's a safe measure, but updates can't be automatic (require SSH or FTP credentials). Ex: FTP: user 522/538; Apache/PHP: 48/48 (www-data)

#!/bin/bash
USER=user
PASSWD=password
SITE=www.example.com
#DIR_MOD=0755
DIR_MOD=0775
#FILE_MOD=0644
FILE_MOD=0664
ROOT_DIR=/public_html

# use `cat` instead of last command after the last pipe for dry-run
{
lftp <<EOF
open -u $USER,$PASSWD $SITE
find $ROOT_DIR
exit
EOF
} | gawk 'BEGIN { print "open -u '$USER','$PASSWD' '$SITE'" } { if (match($0 ,/\/$/)) printf "chmod '$DIR_MOD' \"%s\"\n", $0; else printf "chmod '$FILE_MOD' \"%s\"\n", $0 } END { print "exit" }' | lftp

The Code is in get_filesystem_method(). Wordpress tries to create a file 'wp-content/temp-write-test-'.time()

define( 'FTP_USER', 'username' );
define( 'FTP_PASS', 'password' );
define( 'FTP_HOST', 'ftp.example.org' );

Minify

Test

From westonruter/amp-wp-theme-compat-analysis/start.sh:

#!/bin/bash

set -x
set -e
lando start

if [ ! -e public ]; then
  mkdir public
fi

if [ ! -e public/index.php ]; then
  lando wp core download
fi

if ! lando wp core is-installed; then
  lando wp core install --url="https://amp-wp-theme-compat-analysis.lndo.site/" --title="AMP WP Theme Compatibility Analysis" --admin_user=admin --admin_password=password --admin_email=nobody@example.com
fi

lando wp plugin install --activate amp
lando wp option update --json amp-options '{"theme_support":"standard"}'

lando wp plugin install --activate wordpress-importer
lando wp plugin install --activate block-unit-test
#lando wp plugin install --activate coblocks

if [ ! -e themeunittestdata.wordpress.xml ]; then
  wget https://raw.githubusercontent.com/WPTRT/theme-unit-test/master/themeunittestdata.wordpress.xml
fi
if [[ 0 == $(lando wp menu list --format=count) ]]; then
  lando wp import --authors=create themeunittestdata.wordpress.xml
fi

if [[ 0 == $(lando wp post list --post_type=attachment --post_name=accelerated-mobile-pages-is-now-just-amp --format=count) ]]; then
  wget https://blog.amp.dev/wp-content/uploads/2019/04/only_amp.mp4
  lando wp media import --title="Accelerated Mobile Pages is now just AMP" only_amp.mp4
  rm only_amp.mp4
fi

lando wp create-monster-post
lando wp populate-initial-widgets

bash check-wporg-themes.sh 100

OOP

In wp-content/themes/<themeslug>/functions.php:

<?php
class My_Theme
{
	public function __construct()
	{
		add_action('init', fn() => $this->init());
		//or add_action('init', Closure::fromCallable([$this, 'init']));
	}

	private function init()
	{
		// ...
	}
}

$my_theme = new My_Theme();
?>

Code

External script use WordPress functions

Aka headless, api

<?php
// Hide PHP Notice, Warning, logs...
ini_set('display_errors', 0);

header('Content-Type: application/json');

const WP_USE_THEMES = false;
$parse_uri = explode( 'wp-content', $_SERVER['SCRIPT_FILENAME'] );
require_once( $parse_uri[0] . 'wp-load.php' );
?>

Last updated