WordPress: Beyond the Basics

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

Contents

  1. Performance & Caching
  2. wp-config.php
  3. WP-CLI
  4. Hook System
  5. Custom Post Types & Taxonomies
  6. Block Editor (Gutenberg)
  7. REST API
  8. Multisite
  9. Security Hardening
  10. Deployment & Version Control
  11. Child Themes vs. Plugins
  12. Useful Patterns & Snippets
  13. Debugging Toolkit

01 — Performance & 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:

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.

02 — wp-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

03 — WP-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.

04 — The 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

05 — Custom 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

06 — The 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>Your content here.</p>
<!-- /wp:paragraph -->

This means:

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
}

07 — The 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

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
    ]);
});

08 — Multisite

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

09 — Security Hardening Checklist

Beyond "keep everything updated":

File System

location ~* /uploads/.*\.php$ {
    deny all;
}

Authentication

Monitoring

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=()

10 — Deployment & 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"
  }
}

11 — Child Themes vs. Plugins: The Right Tool

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

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'
    );
});

12 — Useful 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' );

13 — Debugging 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).


WordPress is a platform, not just a CMS. The more you understand what's happening under the hood, the more predictable and controllable it becomes.