WordPress: Beyond the Basics

A reference for experienced users who know their way around the dashboard and want to go deeper.

01Performance & Caching

How WordPress Caching Actually Works

WordPress generates pages dynamically on every request by default — PHP executes, queries run against MySQL, and HTML is assembled. Caching short-circuits this at various layers:

  • Object cache — stores database query results in memory (Redis or Memcached). Without a persistent object cache, WordPress's built-in object cache only lasts for a single page request.
  • Page cache — stores fully-rendered HTML. Plugins like WP Rocket, W3 Total Cache, or LiteSpeed Cache handle this; so do some hosts at the server level (Kinsta, WP Engine, Cloudways).
  • Opcode cache — PHP compiles your .php files to bytecode and caches that in memory (OPcache). Most hosts enable this by default; verify with a phpinfo() page.
  • CDN / edge cache — Cloudflare, Bunny.net, or your host's CDN caches assets and sometimes full pages at the edge.

The most impactful upgrade for a busy site is adding a persistent Redis object cache. Install the Redis Object Cache plugin, point it at your Redis instance, and watch admin-panel queries drop dramatically.

Database Housekeeping

WordPress accumulates dead weight over time. Run these with WP-CLI or a plugin like WP-Optimize:

# Delete post revisions older than 30 days
wp post delete $(wp post list --post_type=revision --format=ids) --force

# Clear transients
wp transient delete --expired

# Optimize tables
wp db optimize

Limit revisions globally by adding this to wp-config.php:

define( 'WP_POST_REVISIONS', 5 );

Query Performance

Install Query Monitor (free, essential) to see exactly which queries are running on each page load, how long they take, and which plugin or theme triggered them. A slow query or an N+1 problem is often the real culprit behind a slow admin or slow front end — not theme bloat.

02wp-config.php: The Levers Most People Ignore

wp-config.php is the most powerful configuration file in a WordPress install. Beyond database credentials, it controls:

// Increase memory limit (check your host's hard ceiling first)
define( 'WP_MEMORY_LIMIT', '256M' );
define( 'WP_MAX_MEMORY_LIMIT', '512M' ); // for admin

// Disable the file editor in the dashboard (security best practice)
define( 'DISALLOW_FILE_EDIT', true );

// Disable all plugin/theme updates and installs (lock down production)
define( 'DISALLOW_FILE_MODS', true );

// Force SSL for admin and logins
define( 'FORCE_SSL_ADMIN', true );

// Enable debug logging to a file instead of the screen
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );   // logs to /wp-content/debug.log
define( 'WP_DEBUG_DISPLAY', false );

// Move wp-content outside the webroot (advanced)
define( 'WP_CONTENT_DIR', '/home/user/wp-content' );
define( 'WP_CONTENT_URL', 'https://example.com/wp-content' );

// Set a custom table prefix (should be done at install, but can be migrated)
$table_prefix = 'xk7_';

// Salts and keys — regenerate at https://api.wordpress.org/secret-key/1.1/salt/
// Rotate these when you suspect a breach; all logged-in users are signed out

03WP-CLI: The Right Way to Manage WordPress at Scale

If you're not using WP-CLI, you're working harder than you need to. It handles almost everything the dashboard does, plus things it can't:

# Update core, all plugins, all themes in one pass
wp core update && wp plugin update --all && wp theme update --all

# Search-replace across the entire database (multisite-safe)
wp search-replace 'http://old-domain.com' 'https://new-domain.com' --precise

# Export/import content
wp export --dir=/tmp/exports
wp import /tmp/exports/file.xml --authors=create

# Create an admin user
wp user create jane jane@example.com --role=administrator --user_pass=SecurePass1

# Run a custom PHP snippet against the live site
wp eval 'echo get_option("blogname");'

# Profile a slow hook
wp profile hook --all --order-by=query_time

# Generate dummy content for testing
wp post generate --count=50 --post_type=product

WP-CLI also accepts --ssh=user@host to run commands on remote servers without logging in first, making it ideal for deployment scripts and CI pipelines.

04The WordPress Hook System

Understanding actions and filters is the difference between hacking core files (bad) and writing maintainable code.

Actions let you run code at a specific point in execution. Filters let you intercept and modify data before it's used.

// Action: run code when a post is published
add_action( 'publish_post', function( $post_id ) {
    // Ping an external webhook, update a cache, etc.
}, 10, 1 ); // priority 10, 1 argument

// Filter: modify the_content before output
add_filter( 'the_content', function( $content ) {
    if ( is_single() ) {
        $content .= '<p>Thanks for reading.</p>';
    }
    return $content; // always return from a filter
} );

// Remove someone else's hook (you need the same priority and callback reference)
remove_action( 'wp_head', 'wp_generator' ); // removes WP version meta tag
remove_filter( 'the_content', [ $some_object, 'method_name' ], 10 );

Hook priorities run lowest-to-highest (default 10). To run after most plugins, use 99 or 100. To run before, use 1 or 5.

Key Hooks to Know

HookTypeWhen it fires
initactionAfter WordPress loads, before headers sent
wp_loadedactionAfter all plugins and theme loaded
template_redirectactionBefore template is chosen — redirect here
wp_enqueue_scriptsactionCorrect place to register/enqueue assets
save_postactionWhen any post type is saved
pre_get_postsaction/filterModify WP_Query before it runs
the_contentfilterFilter post content before display
wp_nav_menu_itemsfilterModify navigation HTML
upload_mimesfilterAllow/block file types in Media

05Custom Post Types & Taxonomies Without a Plugin

Register CPTs in a plugin file (never in functions.php for anything serious):

add_action( 'init', function() {
    register_post_type( 'property', [
        'labels'        => [ 'name' => 'Properties', 'singular_name' => 'Property' ],
        'public'        => true,
        'has_archive'   => true,
        'supports'      => [ 'title', 'editor', 'thumbnail', 'custom-fields' ],
        'menu_icon'     => 'dashicons-building',
        'rewrite'       => [ 'slug' => 'properties' ],
        'show_in_rest'  => true, // required for Gutenberg support
    ]);

    register_taxonomy( 'property_type', 'property', [
        'hierarchical'  => true,  // true = like categories, false = like tags
        'public'        => true,
        'rewrite'       => [ 'slug' => 'property-type' ],
        'show_in_rest'  => true,
    ]);
});

After registering, flush rewrite rules once by visiting Settings → Permalinks, or via WP-CLI:

wp rewrite flush

06The Block Editor (Gutenberg): What's Actually Happening

Gutenberg stores content as HTML with structured comments. A paragraph block looks like this in the database:

<!-- wp:paragraph {"dropCap":true} -->
<p class="has-drop-cap">Your content here.</p>
<!-- /wp:paragraph -->

This means:

  • Classic editor content still works — it's just stored as plain HTML without block comments. Don't fear migration.
  • Block attributes live in the comment, not the HTML. Querying or transforming block content requires parsing those comments, not just the HTML.
  • The Block API is JavaScript-first. Custom blocks are React components registered with registerBlockType(). Server-side rendering is available for dynamic blocks.
  • Block patterns (reusable layout templates) and block variations (pre-configured versions of a block) are the right way to create starter layouts — not custom blocks with hardcoded markup.

To register a server-rendered block without a full webpack build:

// In your plugin's init callback:
register_block_type( 'myplugin/latest-news', [
    'render_callback' => 'myplugin_render_latest_news',
    'attributes'      => [
        'count' => [ 'type' => 'integer', 'default' => 3 ],
    ],
]);

function myplugin_render_latest_news( $attrs ) {
    $posts = get_posts([ 'numberposts' => $attrs['count'] ]);
    // Build and return HTML
}

07The REST API

WordPress ships with a full REST API at /wp-json/wp/v2/. You can use it for headless setups, mobile apps, or JavaScript-heavy front ends.

Useful Endpoints

GET  /wp-json/wp/v2/posts?per_page=10&categories=5
GET  /wp-json/wp/v2/posts/{id}
POST /wp-json/wp/v2/posts          (requires auth)
GET  /wp-json/wp/v2/users/me       (requires auth)

Authentication Options

  • Application Passwords (built-in since 5.6) — generate a password per app in Users → Profile. Send as Authorization: Basic base64(username:app-password).
  • JWT Authentication — requires a plugin (JWT Auth by Useful Team is popular). Better for mobile apps.
  • Cookie + nonce — for JavaScript running on the same site. WordPress injects wpApiSettings.nonce into the page; pass it as X-WP-Nonce header.

Extending the API

// Add a custom field to the posts endpoint
register_rest_field( 'post', 'reading_time', [
    'get_callback' => function( $post_arr ) {
        $words = str_word_count( strip_tags( $post_arr['content']['rendered'] ) );
        return ceil( $words / 200 ); // minutes
    },
    'schema' => [ 'type' => 'integer' ],
]);

// Register a fully custom endpoint
add_action( 'rest_api_init', function() {
    register_rest_route( 'myplugin/v1', '/stats', [
        'methods'             => 'GET',
        'callback'            => 'myplugin_stats_handler',
        'permission_callback' => '__return_true', // or check capabilities
    ]);
});

08Multisite

WordPress Multisite turns a single install into a network of sites sharing one codebase and one set of plugins/themes (though activation is per-site or network-wide).

Enable it by adding to wp-config.php before /* That's all, stop editing! */:

define( 'WP_ALLOW_MULTISITE', true );

Then visit Tools → Network Setup and follow the instructions. You'll add constants to wp-config.php and rewrite rules to .htaccess or your Nginx config.

Architectural Realities

  • Each site gets its own set of database tables (wp_2_posts, wp_3_posts, etc.) but shares wp_users and wp_usermeta.
  • Plugins installed network-wide run on every site. Site-level activation only affects that site.
  • Super Admin is a separate role above Administrator. Only Super Admins can install plugins/themes.
  • Domain mapping (separate domains per subsite) requires either a plugin or server-level configuration.
  • switch_to_blog( $blog_id ) lets you run queries against another site in the network; always call restore_current_blog() afterward.

09Security Hardening Checklist

Beyond "keep everything updated":

File System

  • Set wp-config.php permissions to 600 (owner read/write only)
  • Set directory permissions to 755, file permissions to 644
  • Ensure the web server user cannot write to wp-includes/ or wp-admin/
  • Block direct PHP execution in wp-content/uploads/ via .htaccess or Nginx:
location ~* /uploads/.*\.php$ {
    deny all;
}

Authentication

  • Enforce strong passwords with the built-in strength meter plus a policy plugin
  • Enable two-factor authentication (Two Factor plugin by George Stephanis is audited and free)
  • Limit login attempts (Limit Login Attempts Reloaded, or your host's WAF)
  • Disable XML-RPC if you don't use Jetpack or the mobile app: add_filter( 'xmlrpc_enabled', '__return_false' );
  • Remove the username from ?author=1 enumeration by redirecting author archive queries

Monitoring

  • Enable email alerts for new admin user creation
  • Log file changes (Wordfence or a standalone file integrity monitor)
  • Set up uptime monitoring separately from your host

Security Headers

Add at the server level or via a plugin:

X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), microphone=()

10Deployment & Version Control

Never edit production files directly. A proper workflow looks like:

  1. Local development (LocalWP, Lando, or DDEV)
  2. Git for all code — theme, custom plugins, wp-config.php (with secrets excluded via .env or server environment variables)
  3. Staging environment for testing
  4. Deploy via git pull, rsync, or a CI/CD pipeline (GitHub Actions + WP-CLI is a common setup)

What Goes in Git

✅ Custom theme files
✅ Custom plugin files
✅ composer.json / composer.lock
✅ wp-config.php (without credentials — use environment variables)
✅ .htaccess / nginx.conf snippets

❌ wp-content/uploads/ (media library — sync separately or use offload to S3)
❌ wp-content/plugins/ (manage with Composer or deploy separately)
❌ wp-content/cache/
❌ node_modules/

Manage plugin dependencies with Composer + wpackagist:

{
  "repositories": [
    { "type": "composer", "url": "https://wpackagist.org" }
  ],
  "require": {
    "wpackagist-plugin/query-monitor": "^3.14",
    "wpackagist-plugin/redis-cache": "^2.5"
  }
}

11Child Themes vs. Plugins: The Right Tool

A common mistake is putting all custom code in functions.php. The rule of thumb:

  • Functionality independent of the theme (custom post types, shortcodes, REST endpoints, admin tweaks) → plugin. If you change themes, the functionality survives.
  • Visual or layout changes to a specific parent themechild theme.
  • Global front-end customisations tied to the site's design → child theme's functions.php is acceptable, but a site-specific plugin is still cleaner.

A minimal child theme needs only two files:

my-child-theme/
  style.css         ← must contain the Theme Name and Template header
  functions.php     ← enqueue parent stylesheet here
// functions.php in child theme
add_action( 'wp_enqueue_scripts', function() {
    wp_enqueue_style(
        'parent-style',
        get_template_directory_uri() . '/style.css'
    );
});

12Useful Patterns & Snippets

// Run code only on the front end, not in admin or REST
if ( ! is_admin() && ! wp_is_json_request() ) { ... }

// Get the current page's template file
$template = get_page_template_slug( get_the_ID() );

// Programmatically create or update a post
$post_id = wp_insert_post([
    'post_title'   => 'Auto-generated',
    'post_content' => 'Content here',
    'post_status'  => 'publish',
    'post_type'    => 'post',
    'meta_input'   => [ '_custom_key' => 'value' ],
]);

// Query posts by meta value efficiently (use indexed meta keys)
$query = new WP_Query([
    'post_type'  => 'property',
    'meta_query' => [
        [ 'key' => '_price', 'value' => 500000, 'compare' => '<=', 'type' => 'NUMERIC' ],
    ],
    'orderby' => 'meta_value_num',
    'meta_key' => '_price',
    'order'   => 'ASC',
]);

// Send a transactional email using wp_mail with HTML
add_filter( 'wp_mail_content_type', fn() => 'text/html' );
wp_mail( 'user@example.com', 'Subject', '<p>Hello <strong>World</strong></p>' );
remove_filter( 'wp_mail_content_type', fn() => 'text/html' ); // reset immediately

// Schedule a one-off event
if ( ! wp_next_scheduled( 'myplugin_process_queue' ) ) {
    wp_schedule_single_event( time() + 300, 'myplugin_process_queue' );
}
add_action( 'myplugin_process_queue', 'myplugin_do_processing' );

13Debugging Toolkit

ToolWhat it's for
Query MonitorQueries, hooks, conditionals, HTTP requests per page load
WP-CLI wp profileHook-level profiling from the command line
XdebugStep-through PHP debugging in a local environment
Health Check & TroubleshootingPlugin conflict isolation without affecting visitors
Log ViewerRead debug.log from the dashboard
Browser DevTools → NetworkREST API calls, asset loading, TTFB

Enable structured logging in your own code:

// Only logs when WP_DEBUG_LOG is true
if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
    error_log( print_r( $data, true ) );
}

For persistent, structured logs in production, consider routing through a logging plugin that writes to a dedicated table or an external service (Papertrail, Logtail).