Back to Articles
18 min read

Advanced WordPress Theme Logic: Queries, Customizer API & Internationalization

Moving beyond basic template hierarchy, this guide focuses on the dynamic capabilities of WordPress. We explore the extensibility of Child Themes, complex data retrieval via Advanced `WP_Query`, user-facing configuration through the Customizer API, and the rigorous standards required for Internationalization (i18n) and responsive media handling.

Child Themes

Child theme creation

A child theme inherits functionality from a parent theme while allowing safe customizations that survive parent theme updates. Create a folder in wp-content/themes/, add style.css with required headers, and functions.php to enqueue styles.

wp-content/themes/ ├── parent-theme/ │ ├── style.css │ ├── functions.php │ └── templates/ └── parent-theme-child/ ← Your child theme ├── style.css ← Required (with Template header) ├── functions.php ← Required (enqueue parent styles) └── screenshot.png ← Optional
/* style.css */ /* Theme Name: Parent Theme Child Template: parent-theme Version: 1.0.0 */

Inheriting parent styles

Parent styles must be properly enqueued in the child theme's functions.php using wp_enqueue_style() with the parent's handle as a dependency, ensuring correct CSS cascade order.

// functions.php add_action('wp_enqueue_scripts', 'child_enqueue_styles'); function child_enqueue_styles() { wp_enqueue_style('parent-style', get_template_directory_uri() . '/style.css'); wp_enqueue_style('child-style', get_stylesheet_uri(), array('parent-style')); }

Overriding templates

To override a parent template, copy the file to your child theme maintaining the same relative path and filename; WordPress's template hierarchy automatically prioritizes the child theme version.

TEMPLATE OVERRIDE FLOW: ┌─────────────────────────────────────────────────┐ │ WordPress Request │ │ │ │ │ ▼ │ │ ┌──────────────────┐ Found? │ │ │ Child Theme │───────────► Use it │ │ │ single.php │ YES │ │ └──────────────────┘ │ │ │ NO │ │ ▼ │ │ ┌──────────────────┐ │ │ │ Parent Theme │───────────► Use it │ │ │ single.php │ │ │ └──────────────────┘ │ └─────────────────────────────────────────────────┘

Overriding functions

Parent theme functions can be overridden using pluggable functions (wrapped in function_exists()), removing actions/filters with remove_action()/remove_filter(), or by using higher priority hooks in the child theme.

// Parent theme (pluggable function pattern) if (!function_exists('theme_setup')) { function theme_setup() { // Default implementation } } // Child theme - define before parent loads function theme_setup() { // Your custom implementation - THIS WINS } // Alternative: Remove and re-add actions add_action('after_setup_theme', 'child_override_parent', 20); function child_override_parent() { remove_action('wp_head', 'parent_function_name'); add_action('wp_head', 'my_replacement_function'); }

When to use child themes

Use child themes when customizing third-party themes to preserve modifications during updates, when making extensive template or styling changes, or when multiple sites need variations of the same base theme.

DECISION MATRIX: ┌────────────────────────────────┬───────────────────┐ │ Scenario │ Use Child Theme? │ ├────────────────────────────────┼───────────────────┤ │ Modifying 3rd-party theme │ ✅ YES │ │ Building from scratch │ ❌ NO │ │ Minor CSS tweaks only │ ⚠️ Maybe/Customizer│ │ Override multiple templates │ ✅ YES │ │ Theme you maintain/own │ ❌ NO │ └────────────────────────────────┴───────────────────┘

Custom Page Templates

Template naming convention

WordPress follows a strict template hierarchy: page-{slug}.php for specific slugs, page-{id}.php for specific IDs, or custom templates with header comments; specificity determines which file WordPress loads.

PAGE TEMPLATE HIERARCHY (Most → Least Specific):
┌─────────────────────────────────────────────┐
│ 1. Custom Template (assigned in editor)     │
│ 2. page-{slug}.php                          │
│ 3. page-{id}.php                            │
│ 4. page.php                                 │
│ 5. singular.php                             │
│ 6. index.php                                │
└─────────────────────────────────────────────┘

Example for /about-us/ page (ID: 42):
  page-about-us.php → page-42.php → page.php → index.php

Template header comment

Custom page templates require a PHP comment block at the top declaring Template Name: which makes the template selectable in the WordPress page editor's Template dropdown.

<?php /** * Template Name: Full Width No Sidebar * Template Post Type: page, post, product * Description: A custom full-width template without sidebar */ get_header(); // Template content here get_footer();

Assigning templates to pages

Templates are assigned via the Page Attributes meta box in the block editor (or classic editor), stored as _wp_page_template post meta, and can also be set programmatically using update_post_meta().

// Programmatically assign template update_post_meta($post_id, '_wp_page_template', 'templates/full-width.php'); // Check current template $template = get_page_template_slug($post_id);
WORDPRESS EDITOR:
┌─────────────────────────────────────────┐
│ Page Attributes                     [-] │
├─────────────────────────────────────────┤
│ Template                                │
│ ┌─────────────────────────────────┐    │
│ │ Default Template            ▼   │    │
│ ├─────────────────────────────────┤    │
│ │ Default Template                │    │
│ │ Full Width No Sidebar       ◄───│────── Your custom template
│ │ Landing Page                    │    │
│ └─────────────────────────────────┘    │
└─────────────────────────────────────────┘

Template for custom post types

Custom post types use dedicated templates: single-{post_type}.php for individual posts, archive-{post_type}.php for archives, following WordPress hierarchy with fallbacks to generic templates.

// Register CPT with archive support register_post_type('portfolio', [ 'public' => true, 'has_archive' => true, // Enables archive-portfolio.php 'rewrite' => ['slug' => 'portfolio'], ]);
CPT TEMPLATE HIERARCHY:
┌─────────────────────────────────────────────────────┐
│ SINGLE:                                             │
│   single-portfolio.php → single.php → singular.php  │
│                                                     │
│ ARCHIVE:                                            │
│   archive-portfolio.php → archive.php → index.php   │
│                                                     │
│ TAXONOMY (custom):                                  │
│   taxonomy-portfolio_cat-term.php                   │
│   taxonomy-portfolio_cat.php                        │
│   taxonomy.php → archive.php                        │
└─────────────────────────────────────────────────────┘

Conditional Tags

is_home()

Returns true when viewing the blog posts index page (the page showing latest posts), which may differ from the front page if a static front page is configured in Settings → Reading.

if (is_home()) { // Blog posts index page echo '<h1>Latest Blog Posts</h1>'; }
COMMON CONFUSION:
┌────────────────────────────────────────────────────┐
│ Settings → Reading: "Your homepage displays"      │
├────────────────────────────────────────────────────┤
│ "Your latest posts":                              │
│   Front page: is_home()=TRUE, is_front_page()=TRUE│
│                                                    │
│ "A static page":                                  │
│   Homepage:  is_front_page()=TRUE, is_home()=FALSE│
│   Posts page: is_home()=TRUE, is_front_page()=FALSE│
└────────────────────────────────────────────────────┘

is_front_page()

Returns true when viewing the site's front page regardless of whether it displays posts or a static page, determined by Settings → Reading configuration.

if (is_front_page() && !is_home()) { // Static front page (not the blog) get_template_part('hero', 'front-page'); }

is_single()

Returns true when viewing a single post of any post type (except attachments and pages), optionally accepting post ID, title, slug, or array to check for specific posts.

is_single(); // Any single post is_single(42); // Post with ID 42 is_single('my-post'); // Post with slug 'my-post' is_single(['news', 42]); // Post with slug 'news' OR ID 42 // Common usage if (is_single() && in_category('featured')) { get_template_part('content', 'featured'); }

is_page()

Returns true when viewing a static page (not posts), optionally accepting page ID, title, slug, or array to check for specific pages.

is_page(); // Any page is_page(42); // Page ID 42 is_page('about'); // Page slug 'about' is_page(['about', 'contact']);// Either page if (is_page('contact')) { wp_enqueue_script('contact-form'); }

is_category()

Returns true when viewing a category archive page, optionally accepting category ID, name, slug, or array to check specific categories.

is_category(); // Any category archive is_category(5); // Category ID 5 is_category('news'); // Category slug/name 'news' is_category(['news', 'blog']); if (is_category('tutorials')) { get_sidebar('tutorials'); }

is_tag()

Returns true when viewing a tag archive page, optionally accepting tag ID, name, slug, or array to check specific tags.

is_tag(); // Any tag archive is_tag('featured'); // Specific tag is_tag([5, 6, 'important']); // Multiple tags if (is_tag()) { echo '<p>Posts tagged: ' . single_tag_title('', false) . '</p>'; }

is_archive()

Returns true when viewing any archive page including category, tag, author, date, custom post type archives, and custom taxonomy archives.

is_archive(); // TRUE for all archive types // Archive type breakdown if (is_archive()) { if (is_category()) { /* category */ } elseif (is_tag()) { /* tag */ } elseif (is_author()) { /* author */ } elseif (is_date()) { /* date */ } elseif (is_tax()) { /* custom taxonomy */ } elseif (is_post_type_archive()) { /* CPT archive */ } }

is_search()

Returns true when viewing search results page, commonly used to display search-specific layouts or messages when no results are found.

if (is_search()) { global $wp_query; printf( '<h1>%d results for: %s</h1>', $wp_query->found_posts, get_search_query() ); }

is_404()

Returns true when WordPress cannot find any content matching the request, triggering the 404 template; useful for custom error pages or redirects.

if (is_404()) { // Log 404 errors or suggest content error_log('404: ' . $_SERVER['REQUEST_URI']); // Display helpful suggestions get_search_form(); wp_list_pages(['title_li' => 'Popular Pages']); }

is_user_logged_in()

Returns true when a user is currently logged in, essential for displaying member-only content, showing/hiding login links, or restricting functionality.

if (is_user_logged_in()) { $user = wp_get_current_user(); echo "Welcome, {$user->display_name}!"; wp_nav_menu(['theme_location' => 'member-menu']); } else { wp_login_form(); }

is_admin()

Returns true when the current request is for an admin page (wp-admin), but note it returns true during AJAX requests; use is_admin() && !wp_doing_ajax() for strict admin-only checks.

// Only load on frontend if (!is_admin()) { add_action('wp_enqueue_scripts', 'frontend_scripts'); } // Be careful with AJAX! if (is_admin() && !wp_doing_ajax()) { // Truly admin screens only }

is_singular()

Returns true when viewing any single post, page, or attachment; optionally accepts post type(s) to check specific types, making it more flexible than combining is_single() and is_page().

is_singular(); // Any single content is_singular('page'); // Same as is_page() is_singular('post'); // Same as is_single() is_singular('product'); // Single product CPT is_singular(['post', 'page', 'product']); // Any of these if (is_singular() && comments_open()) { comments_template(); }

in_category()

Checks if the current post (or specified post) belongs to a given category, working inside the loop or with a post ID parameter; distinct from is_category() which checks archive pages.

// Inside the loop if (in_category('featured')) { echo '<span class="badge">Featured</span>'; } // Outside the loop with post ID if (in_category('news', $post_id)) { // Post belongs to 'news' category } // Multiple categories (OR logic) if (in_category(['breaking', 'urgent'])) { add_action('wp_head', 'add_urgent_styles'); }

has_tag()

Checks if the current post (or specified post) has a given tag or any tag if no parameter is passed, functioning within the loop similar to in_category().

// Has any tag? if (has_tag()) { the_tags('<div class="tags">', ', ', '</div>'); } // Has specific tag? if (has_tag('featured')) { echo '<span class="star">★</span>'; } // Multiple tags if (has_tag(['sponsored', 'ad'])) { echo '<div class="disclosure">Sponsored content</div>'; }
CONDITIONAL TAGS CHEAT SHEET:
┌─────────────────┬────────────────────────────────────┐
│ Function        │ When it returns TRUE               │
├─────────────────┼────────────────────────────────────┤
│ is_front_page() │ Site front page                    │
│ is_home()       │ Blog posts index                   │
│ is_single()     │ Single post (not page)             │
│ is_page()       │ Static page                        │
│ is_singular()   │ Any single content                 │
│ is_archive()    │ Any archive listing                │
│ is_category()   │ Category archive page              │
│ in_category()   │ Post belongs to category           │
│ is_tag()        │ Tag archive page                   │
│ has_tag()       │ Post has tag assigned              │
└─────────────────┴────────────────────────────────────┘

Advanced WP_Query

Query arguments

WP_Query accepts an array of arguments controlling what posts to fetch, including post_type, posts_per_page, post_status, author, category, and dozens more parameters for precise content retrieval.

$args = [ 'post_type' => 'post', 'post_status' => 'publish', 'posts_per_page' => 10, 'author' => 1, 'category_name' => 'news', 'tag' => 'featured', 'offset' => 5, 'ignore_sticky_posts' => true, 'no_found_rows' => true, // Performance: skip pagination calc ]; $query = new WP_Query($args);

Custom queries

Custom queries allow fetching posts independently of the main query, useful for related posts, featured sections, or widgets; always reset with wp_reset_postdata() after custom loops.

$featured = new WP_Query([ 'post_type' => 'post', 'posts_per_page' => 3, 'meta_key' => 'featured', 'meta_value' => '1', ]); if ($featured->have_posts()) : while ($featured->have_posts()) : $featured->the_post(); get_template_part('template-parts/card', 'featured'); endwhile; wp_reset_postdata(); // ← CRITICAL: Restore global $post endif;

Meta queries

Meta queries filter posts by custom field values with support for comparison operators, type casting, and multiple conditions combined with AND/OR relation logic.

$args = [ 'post_type' => 'product', 'meta_query' => [ 'relation' => 'AND', [ 'key' => 'price', 'value' => 100, 'type' => 'NUMERIC', 'compare' => '<=', ], [ 'key' => 'in_stock', 'value' => '1', 'compare' => '=', ], [ 'key' => 'color', 'value' => ['red', 'blue'], 'compare' => 'IN', ], ], ];
META QUERY COMPARE OPERATORS:
┌───────────────┬──────────────────────────────────┐
│ Operator      │ Description                      │
├───────────────┼──────────────────────────────────┤
│ =             │ Equal (default)                  │
│ !=            │ Not equal                        │
│ > < >= <=     │ Numeric comparisons              │
│ LIKE          │ Contains (with % wildcards)      │
│ NOT LIKE      │ Does not contain                 │
│ IN            │ In array of values               │
│ NOT IN        │ Not in array                     │
│ BETWEEN       │ Between two values               │
│ NOT BETWEEN   │ Outside range                    │
│ EXISTS        │ Meta key exists                  │
│ NOT EXISTS    │ Meta key doesn't exist           │
└───────────────┴──────────────────────────────────┘

Tax queries

Tax queries filter posts by taxonomy terms with support for multiple taxonomies, operators (IN, NOT IN, AND, EXISTS), and nested conditions via relation parameter.

$args = [ 'post_type' => 'product', 'tax_query' => [ 'relation' => 'AND', [ 'taxonomy' => 'product_cat', 'field' => 'slug', // 'term_id', 'slug', or 'name' 'terms' => ['clothing', 'accessories'], 'operator' => 'IN', ], [ 'taxonomy' => 'product_tag', 'field' => 'slug', 'terms' => ['sale'], 'operator' => 'IN', ], [ 'taxonomy' => 'brand', 'operator' => 'EXISTS', // Has any brand assigned ], ], ];

Date queries

Date queries filter posts by publish date with support for before/after, specific date components (year, month, day, hour), inclusive ranges, and column selection.

$args = [ 'post_type' => 'post', 'date_query' => [ 'relation' => 'AND', [ 'after' => '2024-01-01', 'before' => '2024-12-31', 'inclusive' => true, ], [ 'hour' => 9, 'compare' => '>=', ], [ 'dayofweek' => [1, 2, 3, 4, 5], // Weekdays only 'compare' => 'IN', ], ], 'column' => 'post_date', // or 'post_modified' ];

Orderby parameters

The orderby parameter sorts query results by various fields including date, title, meta values, menu order, random, or multiple fields with order controlling ASC/DESC direction.

// Simple ordering $args = ['orderby' => 'title', 'order' => 'ASC']; // Multiple orderby $args = [ 'orderby' => [ 'menu_order' => 'ASC', 'date' => 'DESC', ], ]; // Order by meta value $args = [ 'meta_key' => 'price', 'orderby' => 'meta_value_num', 'order' => 'ASC', ]; // Named meta queries for complex ordering $args = [ 'meta_query' => [ 'price_clause' => ['key' => 'price', 'type' => 'NUMERIC'], 'rating_clause' => ['key' => 'rating', 'type' => 'NUMERIC'], ], 'orderby' => [ 'rating_clause' => 'DESC', 'price_clause' => 'ASC', ], ];
ORDERBY OPTIONS:
┌──────────────────┬─────────────────────────────────┐
│ Value            │ Order by                        │
├──────────────────┼─────────────────────────────────┤
│ date             │ Post date (default)             │
│ modified         │ Last modified date              │
│ title            │ Post title                      │
│ name             │ Post slug                       │
│ ID               │ Post ID                         │
│ author           │ Author ID                       │
│ rand             │ Random order                    │
│ menu_order       │ Page order value                │
│ meta_value       │ Custom field (alphabetical)     │
│ meta_value_num   │ Custom field (numerical)        │
│ post__in         │ Preserve ID array order         │
└──────────────────┴─────────────────────────────────┘

Pagination with custom queries

Custom query pagination requires passing the correct paged parameter extracted from get_query_var() and using $query->max_num_pages for generating pagination links.

$paged = get_query_var('paged') ? get_query_var('paged') : 1; $custom_query = new WP_Query([ 'post_type' => 'portfolio', 'posts_per_page' => 9, 'paged' => $paged, ]); if ($custom_query->have_posts()) : while ($custom_query->have_posts()) : $custom_query->the_post(); // Content endwhile; // Pagination for custom query echo paginate_links([ 'total' => $custom_query->max_num_pages, 'current' => $paged, ]); wp_reset_postdata(); endif;

pre_get_posts hook

The pre_get_posts action modifies queries before execution, allowing you to alter the main query or specific queries without template edits; always check $query->is_main_query() to avoid unintended modifications.

add_action('pre_get_posts', 'customize_main_query'); function customize_main_query($query) { // Don't modify admin queries if (is_admin()) return; // Only modify main query if (!$query->is_main_query()) return; // Customize blog home if ($query->is_home()) { $query->set('posts_per_page', 12); $query->set('ignore_sticky_posts', true); } // Customize search if ($query->is_search()) { $query->set('post_type', ['post', 'page', 'product']); } // Customize CPT archive if ($query->is_post_type_archive('portfolio')) { $query->set('orderby', 'menu_order'); $query->set('order', 'ASC'); } }
pre_get_posts FLOW:
┌─────────────────────────────────────────────────────────┐
│                    WordPress Request                     │
│                           │                              │
│                           ▼                              │
│                  ┌─────────────────┐                    │
│                  │ Parse Request   │                    │
│                  └────────┬────────┘                    │
│                           ▼                              │
│  ┌──────────────────────────────────────────────────┐   │
│  │          pre_get_posts ACTION FIRES              │◄──│── Modify here!
│  │  $query->set('posts_per_page', 20);              │   │
│  │  $query->set('meta_query', [...]);               │   │
│  └──────────────────────────────────────────────────┘   │
│                           │                              │
│                           ▼                              │
│                  ┌─────────────────┐                    │
│                  │ Execute Query   │                    │
│                  └─────────────────┘                    │
└─────────────────────────────────────────────────────────┘

Pagination

the_posts_pagination()

A modern WordPress function that outputs a complete, accessible pagination block with previous/next links and page numbers; ideal for archive pages using the main query.

the_posts_pagination([ 'mid_size' => 2, 'prev_text' => '← Previous', 'next_text' => 'Next →', 'screen_reader_text' => 'Posts navigation', 'aria_label' => 'Posts', 'class' => 'pagination-wrapper', ]);
<!-- Output --> <nav class="navigation pagination pagination-wrapper" aria-label="Posts"> <h2 class="screen-reader-text">Posts navigation</h2> <div class="nav-links"> <a class="prev page-numbers" href="/page/1/">← Previous</a> <a class="page-numbers" href="/page/1/">1</a> <span class="page-numbers current">2</span> <a class="page-numbers" href="/page/3/">3</a> <a class="next page-numbers" href="/page/3/">Next →</a> </div> </nav>

paginate_links()

A flexible function returning pagination HTML as a string or array, supporting extensive customization for format, base URL, query args, and styling; works with both main and custom queries.

$pagination = paginate_links([ 'base' => str_replace(999999, '%#%', esc_url(get_pagenum_link(999999))), 'format' => '?paged=%#%', 'current' => max(1, get_query_var('paged')), 'total' => $wp_query->max_num_pages, 'type' => 'array', // 'plain', 'array', or 'list' 'prev_text' => '«', 'next_text' => '»', 'end_size' => 1, 'mid_size' => 2, ]); if ($pagination) { echo '<ul class="custom-pagination">'; foreach ($pagination as $page) { echo '<li>' . $page . '</li>'; } echo '</ul>'; }

next_posts_link()

Outputs a link to the next page of posts (older posts in reverse chronological order); accepts custom link text and optional max pages parameter to limit pagination.

// Simple usage next_posts_link('Older Posts →'); // With custom query's max pages next_posts_link('Older Posts →', $custom_query->max_num_pages);

previous_posts_link()

Outputs a link to the previous page of posts (newer posts); unlike next_posts_link(), it doesn't require max pages parameter since it always knows if earlier pages exist.

previous_posts_link('← Newer Posts'); // Complete simple pagination <div class="post-navigation"> <div class="prev"><?php previous_posts_link('← Newer'); ?></div> <div class="next"><?php next_posts_link('Older →'); ?></div> </div>
PAGINATION DIRECTION (Confusing but correct!):
┌─────────────────────────────────────────────────────────┐
│ Page 1          Page 2          Page 3          Page 4 │
│ [newest]                                       [oldest] │
│                                                         │
│  ←───────────────────────────────────────────────────→  │
│  previous_posts_link()              next_posts_link()   │
│  (Newer/Back)                       (Older/Forward)     │
└─────────────────────────────────────────────────────────┘

Custom pagination

Custom pagination involves building your own navigation using paginate_links() with custom markup, AJAX-loaded pages, or infinite scroll implementations for complete design control.

function custom_numeric_pagination($query = null) { $query = $query ?: $GLOBALS['wp_query']; $total = $query->max_num_pages; if ($total <= 1) return; $current = max(1, get_query_var('paged')); $output = '<nav class="pagination" role="navigation">'; // First page if ($current > 2) { $output .= '<a href="' . get_pagenum_link(1) . '" class="first">«</a>'; } // Previous if ($current > 1) { $output .= '<a href="' . get_pagenum_link($current - 1) . '" class="prev">‹</a>'; } // Page numbers for ($i = max(1, $current - 2); $i <= min($total, $current + 2); $i++) { if ($i == $current) { $output .= '<span class="current">' . $i . '</span>'; } else { $output .= '<a href="' . get_pagenum_link($i) . '">' . $i . '</a>'; } } // Next if ($current < $total) { $output .= '<a href="' . get_pagenum_link($current + 1) . '" class="next">›</a>'; } // Last page if ($current < $total - 1) { $output .= '<a href="' . get_pagenum_link($total) . '" class="last">»</a>'; } $output .= '</nav>'; echo $output; }
CUSTOM PAGINATION OUTPUT:
┌─────────────────────────────────────────────────────────┐
│                                                         │
│     « ‹  [1]  2   3   4   5  ›  »                      │
│     ↑ ↑   ↑               ↑  ↑  ↑                      │
│     │ │   │               │  │  └─ Last page           │
│     │ │   │               │  └──── Next page           │
│     │ │   │               └─────── Page links          │
│     │ │   └─────────────────────── Current page        │
│     │ └─────────────────────────── Previous            │
│     └───────────────────────────── First page          │
│                                                         │
└─────────────────────────────────────────────────────────┘

Quick Reference Summary

WORDPRESS THEME DEVELOPMENT ARCHITECTURE:
┌─────────────────────────────────────────────────────────────┐
│                     WORDPRESS REQUEST                        │
└──────────────────────────┬──────────────────────────────────┘
                           │
                           ▼
┌──────────────────────────────────────────────────────────────┐
│                    TEMPLATE HIERARCHY                         │
│  ┌─────────────────────────────────────────────────────────┐ │
│  │ Conditional Tags determine which template to load       │ │
│  │ is_home() → is_single() → is_page() → is_archive()     │ │
│  └─────────────────────────────────────────────────────────┘ │
└──────────────────────────┬───────────────────────────────────┘
                           │
          ┌────────────────┴────────────────┐
          ▼                                 ▼
┌──────────────────────┐         ┌──────────────────────┐
│    CHILD THEME       │         │    PARENT THEME      │
│  ┌────────────────┐  │         │  ┌────────────────┐  │
│  │ Custom Template│──│────┐    │  │ Default Files  │  │
│  │ Override       │  │    │    │  └────────────────┘  │
│  └────────────────┘  │    │    │                      │
│  ┌────────────────┐  │    │    │  ┌────────────────┐  │
│  │ functions.php  │  │    └────│─▶│ Falls back to  │  │
│  │ (Extends)      │  │         │  │ parent         │  │
│  └────────────────┘  │         │  └────────────────┘  │
└──────────────────────┘         └──────────────────────┘
                           │
                           ▼
┌──────────────────────────────────────────────────────────────┐
│                       WP_QUERY                                │
│  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐            │
│  │ meta_query  │ │ tax_query   │ │ date_query  │            │
│  └─────────────┘ └─────────────┘ └─────────────┘            │
│                                                              │
│  pre_get_posts → Execute Query → The Loop → Pagination      │
└──────────────────────────────────────────────────────────────┘

Comments

comments_template()

Loads the comments template file (comments.php) within single post/page templates; it handles checking if comments are open and loads the appropriate template file with all necessary comment data pre-populated.

// In single.php if ( comments_open() || get_comments_number() ) { comments_template(); // Loads /comments.php }

Comment form customization

The comment_form() function accepts an array of arguments to customize every aspect of the form including fields, labels, submit button, and wrapper elements—allowing complete control over markup and functionality.

comment_form( array( 'title_reply' => 'Leave a Reply', 'label_submit' => 'Post Comment', 'comment_field' => '<textarea name="comment" id="comment" rows="4"></textarea>', 'logged_in_as' => '', 'comment_notes_before' => '', ) );

wp_list_comments()

Displays a formatted list of comments with extensive options for styling, callbacks, avatars, and reply links—typically called inside comments.php to render the comment thread.

wp_list_comments( array( 'style' => 'ol', 'short_ping' => true, 'avatar_size' => 50, 'callback' => 'custom_comment_callback', // Custom function ) );

Comment walker class

Extends Walker_Comment to provide complete control over comment HTML structure, enabling custom markup for each comment level—useful when default callbacks aren't flexible enough.

class Custom_Comment_Walker extends Walker_Comment { protected function html5_comment( $comment, $depth, $args ) { ?> <li <?php comment_class(); ?> id="comment-<?php comment_ID(); ?>"> <article class="comment-body"> <?php echo get_avatar( $comment, 60 ); ?> <div class="comment-content"><?php comment_text(); ?></div> </article> <?php } } // Usage: wp_list_comments( array( 'walker' => new Custom_Comment_Walker() ) );

Comment pagination

Splits comments across multiple pages using paginate_comments_links() or manual functions; requires enabling paged comments in Settings → Discussion with comments_per_page option.

// In comments.php the_comments_pagination( array( 'prev_text' => '← Previous', 'next_text' => 'Next →', ) ); // Or manual approach echo paginate_comments_links( array( 'echo' => false ) );

Threaded comments

Enables nested replies where users can respond directly to specific comments; requires theme support declaration and proper script enqueuing for the reply link functionality.

// functions.php add_theme_support( 'threaded-comments' ); // Enqueue reply script if ( is_singular() && comments_open() ) { wp_enqueue_script( 'comment-reply' ); }
Comment Thread Structure:
┌─────────────────────────────┐
│ Comment #1 (depth: 1)       │
│  ├── Reply #1.1 (depth: 2)  │
│  │    └── Reply #1.1.1      │
│  └── Reply #1.2 (depth: 2)  │
│ Comment #2 (depth: 1)       │
└─────────────────────────────┘

Post Thumbnails/Featured Images

set_post_thumbnail_size()

Defines the default dimensions for the post-thumbnail image size used by the_post_thumbnail(); should be called after add_theme_support('post-thumbnails') in functions.php.

add_theme_support( 'post-thumbnails' ); set_post_thumbnail_size( 800, 450, true ); // width, height, hard crop

add_image_size()

Registers custom image sizes that WordPress generates during upload; useful for creating theme-specific dimensions for different layout contexts like grids, sliders, or cards.

add_image_size( 'hero-banner', 1920, 600, true ); // Hard crop add_image_size( 'card-thumb', 400, 300, true ); add_image_size( 'portfolio', 600, 0 ); // Flexible height // To use in media library dropdown: add_filter( 'image_size_names_choose', function( $sizes ) { return array_merge( $sizes, array( 'hero-banner' => 'Hero Banner' ) ); } );

the_post_thumbnail()

Outputs the featured image HTML within The Loop; accepts size parameter and optional attributes array for adding classes, alt text, or other HTML attributes.

// Basic usage the_post_thumbnail(); // With size and attributes the_post_thumbnail( 'card-thumb', array( 'class' => 'featured-img lazy', 'alt' => get_the_title(), 'loading' => 'lazy', ) );

get_the_post_thumbnail_url()

Returns only the URL string of the featured image (not full HTML), useful for CSS backgrounds, JavaScript, or when you need custom image markup.

$thumb_url = get_the_post_thumbnail_url( get_the_ID(), 'large' ); // Usage in inline style <div style="background-image: url('<?php echo esc_url( $thumb_url ); ?>')"> // Check and fallback $image = get_the_post_thumbnail_url() ?: get_template_directory_uri() . '/images/default.jpg';

Responsive images

WordPress automatically generates srcset and sizes attributes for images inserted via the_post_thumbnail() or content, enabling browsers to select optimal image sizes based on viewport.

// WordPress auto-generates this output: <img src="image-800x600.jpg" srcset="image-400x300.jpg 400w, image-800x600.jpg 800w, image-1200x900.jpg 1200w" sizes="(max-width: 800px) 100vw, 800px"> // Filter to customize sizes attribute add_filter( 'wp_calculate_image_sizes', function( $sizes, $size ) { return '(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 800px'; }, 10, 2 );

srcset and sizes

HTML attributes that define available image sources and display conditions; srcset lists image files with widths, sizes tells browser how large the image will display at various breakpoints.

┌─────────────────────────────────────────────────────────────┐
│  srcset = "small.jpg 400w, medium.jpg 800w, large.jpg 1200w"│
│                                                             │
│  sizes  = "(max-width: 600px) 100vw,                        │
│            (max-width: 1000px) 50vw,                        │
│            800px"                                           │
│                                                             │
│  Browser Logic:                                             │
│  ┌──────────────┬──────────────┬──────────────────────────┐ │
│  │ Viewport     │ Display Size │ Chosen Image             │ │
│  ├──────────────┼──────────────┼──────────────────────────┤ │
│  │ 400px        │ 400px (100vw)│ small.jpg (400w)         │ │
│  │ 800px        │ 400px (50vw) │ medium.jpg (2x density)  │ │
│  │ 1200px+      │ 800px fixed  │ large.jpg                │ │
│  └──────────────┴──────────────┴──────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

Theme Customizer API

add_panel()

Creates a top-level container to group related sections in the Customizer; useful for organizing complex themes with many options into logical categories.

$wp_customize->add_panel( 'theme_options', array( 'title' => 'Theme Options', 'description' => 'Configure theme settings', 'priority' => 10, ) );

add_section()

Adds a collapsible section within the Customizer sidebar (optionally inside a panel) that contains related controls; this is the primary organizational unit users interact with.

$wp_customize->add_section( 'header_section', array( 'title' => 'Header Settings', 'panel' => 'theme_options', // Parent panel 'priority' => 10, ) );

add_setting()

Registers a customizer setting that stores a value in the database; defines default value, sanitization, and transport method—settings are the data layer of the Customizer.

$wp_customize->add_setting( 'header_bg_color', array( 'default' => '#ffffff', 'sanitize_callback' => 'sanitize_hex_color', 'transport' => 'postMessage', ) );

add_control()

Creates the UI element that users interact with to modify a setting; links a setting to a visual control type like text input, color picker, or dropdown.

$wp_customize->add_control( 'header_bg_color', array( 'label' => 'Header Background', 'section' => 'header_section', 'type' => 'color', 'settings' => 'header_bg_color', ) );

Customizer control types

WordPress provides built-in control types for various input needs; use standard types or specialized classes for complex inputs like colors, images, and dropdowns.

// Standard types: 'text', 'email', 'url', 'number', 'checkbox', // 'radio', 'select', 'textarea', 'dropdown-pages' // Special control classes new WP_Customize_Color_Control( $wp_customize, 'link_color', array(...) ); new WP_Customize_Image_Control( $wp_customize, 'logo', array(...) ); new WP_Customize_Media_Control( $wp_customize, 'video', array(...) ); new WP_Customize_Cropped_Image_Control( $wp_customize, 'header', array(...) );
┌──────────────────────────────────────┐
│ Control Type Hierarchy               │
├──────────────────────────────────────┤
│ WP_Customize_Control                 │
│  ├── WP_Customize_Color_Control      │
│  ├── WP_Customize_Image_Control      │
│  │    └── WP_Customize_Cropped_Image │
│  ├── WP_Customize_Media_Control      │
│  └── WP_Customize_Code_Editor        │
└──────────────────────────────────────┘

Sanitization callbacks

Functions that validate and clean user input before saving to database; essential for security—every setting must have appropriate sanitization matching its expected data type.

// Built-in sanitizers 'sanitize_callback' => 'sanitize_hex_color' // Colors 'sanitize_callback' => 'absint' // Positive integers 'sanitize_callback' => 'sanitize_text_field' // Plain text 'sanitize_callback' => 'wp_kses_post' // HTML content 'sanitize_callback' => 'esc_url_raw' // URLs // Custom sanitizer for select/radio function sanitize_select( $input, $setting ) { $choices = $setting->manager->get_control( $setting->id )->choices; return array_key_exists( $input, $choices ) ? $input : $setting->default; }

Transport methods (refresh/postMessage)

Determines how changes appear in the preview; refresh reloads the entire preview frame while postMessage uses JavaScript for instant updates without page reload.

// Setting with postMessage transport $wp_customize->add_setting( 'site_title_color', array( 'transport' => 'postMessage', // or 'refresh' (default) ) ); // Required JS in customizer-preview.js wp.customize( 'site_title_color', function( value ) { value.bind( function( newval ) { $( '.site-title' ).css( 'color', newval ); }); }); // Enqueue preview script add_action( 'customize_preview_init', function() { wp_enqueue_script( 'theme-customizer', get_template_directory_uri() . '/js/customizer-preview.js', array( 'customize-preview' ), '', true ); });

Selective refresh

Hybrid approach that uses AJAX to refresh only specific parts of the page; combines the reliability of server-rendered content with near-instant preview updates.

$wp_customize->selective_refresh->add_partial( 'blogname', array( 'selector' => '.site-title a', 'render_callback' => function() { return get_bloginfo( 'name' ); }, 'container_inclusive' => false, ) );
┌─────────────────────────────────────────────────────────────┐
│ Transport Comparison                                        │
├─────────────────┬─────────────────┬─────────────────────────┤
│ refresh         │ postMessage     │ selective_refresh       │
├─────────────────┼─────────────────┼─────────────────────────┤
│ Full page reload│ JS DOM update   │ AJAX partial update     │
│ Slow            │ Instant         │ Fast                    │
│ Always accurate │ May drift       │ Server-rendered         │
│ No extra code   │ Needs JS        │ Needs partial + callback│
└─────────────────┴─────────────────┴─────────────────────────┘

Custom controls

Extend WP_Customize_Control class to create complex UI elements not available in core; allows custom HTML, JavaScript interactions, and specialized input handling.

class Range_Slider_Control extends WP_Customize_Control { public $type = 'range-slider'; public function render_content() { ?> <label> <span class="customize-control-title"><?php echo esc_html( $this->label ); ?></span> <input type="range" min="<?php echo $this->input_attrs['min']; ?>" max="<?php echo $this->input_attrs['max']; ?>" <?php $this->link(); ?> value="<?php echo esc_attr( $this->value() ); ?>"> <output><?php echo esc_html( $this->value() ); ?></output> </label> <?php } } // Usage $wp_customize->add_control( new Range_Slider_Control( $wp_customize, 'font_size', array( 'label' => 'Font Size', 'section' => 'typography', 'input_attrs' => array( 'min' => 12, 'max' => 24 ), )));

Internationalization (i18n)

Text domain

Unique identifier string that links your theme/plugin translations to the correct .mo language files; must match the slug defined in theme/plugin header and be loaded via load_theme_textdomain().

/* * Theme Name: My Theme * Text Domain: my-theme <-- Must match everywhere */ // functions.php add_action( 'after_setup_theme', function() { load_theme_textdomain( 'my-theme', get_template_directory() . '/languages' ); });

__()

Returns a translated string without echoing; use when you need to store, manipulate, or pass the translated text to another function rather than outputting directly.

$message = __( 'Welcome to our site', 'my-theme' ); echo '<h1>' . $message . '</h1>'; // Common pattern with escaping echo '<a href="#">' . esc_html__( 'Read More', 'my-theme' ) . '</a>';

_e()

Echoes a translated string directly; shorthand for echo __() used when you simply need to output translated text without any additional processing.

<h1><?php _e( 'Welcome to our site', 'my-theme' ); ?></h1> <button><?php _e( 'Submit', 'my-theme' ); ?></button> // Note: For escaped output, use: echo esc_html__( 'Text', 'my-theme' ); // No _e equivalent with escaping

_x()

Provides contextual translation where the same English word might translate differently based on usage; the context string helps translators choose the correct meaning.

// "Post" as verb vs noun $verb = _x( 'Post', 'verb: to publish', 'my-theme' ); // "Publicar" $noun = _x( 'Post', 'noun: blog entry', 'my-theme' ); // "Entrada" // "Order" context _x( 'Order', 'sorting order', 'my-theme' ); // Orden _x( 'Order', 'purchase order', 'my-theme' ); // Pedido

_n()

Handles plural forms which vary significantly across languages; takes singular, plural, count, and text domain—essential for numeric outputs.

$count = 5; printf( _n( '%d comment', '%d comments', $count, 'my-theme' ), $count ); // Output: "5 comments" // More complex example $text = sprintf( _n( 'Last %d day', 'Last %d days', $days, 'my-theme' ), $days );
┌────────────────────────────────────────────┐
│ Language Plural Rules Vary!                │
├────────────────────────────────────────────┤
│ English: 1=singular, other=plural          │
│ French:  0-1=singular, other=plural        │
│ Russian: Complex rules (1,2-4,5-20,21...)  │
│ Arabic:  6 different plural forms!         │
└────────────────────────────────────────────┘

esc_html__()

Returns translated string with HTML entities escaped; use in any context where the text appears in HTML content to prevent XSS attacks.

echo '<p>' . esc_html__( 'User <script> input', 'my-theme' ) . '</p>'; // Output: User &lt;script&gt; input // Proper pattern <div class="message"> <?php echo esc_html__( 'Settings saved successfully.', 'my-theme' ); ?> </div>

esc_attr__()

Returns translated string escaped for safe use in HTML attributes; similar to esc_html__() but specifically for attribute contexts like title, alt, value.

<input type="submit" value="<?php echo esc_attr__( 'Search', 'my-theme' ); ?>" title="<?php echo esc_attr__( 'Click to search', 'my-theme' ); ?>"> <img alt="<?php echo esc_attr__( 'Company Logo', 'my-theme' ); ?>">
┌────────────────────────────────────────────────────────┐
│ i18n Function Cheat Sheet                              │
├────────────────────────────────────────────────────────┤
│ __()          → Return translated string               │
│ _e()          → Echo translated string                 │
│ _x()          → Return with context                    │
│ _ex()         → Echo with context                      │
│ _n()          → Return plural form                     │
│ _nx()         → Return plural with context             │
│ esc_html__()  → Return escaped for HTML content        │
│ esc_attr__()  → Return escaped for attributes          │
│ esc_html_e()  → Echo escaped for HTML content          │
│ esc_attr_e()  → Echo escaped for attributes            │
└────────────────────────────────────────────────────────┘

POT file generation

POT (Portable Object Template) files are master translation templates extracted from source code; generated using WP-CLI, Poedit, or build tools, then distributed for translators to create PO/MO files.

# Using WP-CLI (recommended) wp i18n make-pot . languages/my-theme.pot --domain=my-theme # File structure /languages/ ├── my-theme.pot # Template (distribute this) ├── my-theme-es_ES.po # Spanish translations (editable) └── my-theme-es_ES.mo # Spanish compiled (used by WP)
┌─────────────────────────────────────────────────────┐
│ Translation Workflow                                │
│                                                     │
│  Source Code → POT → Translator → PO → Compile → MO│
│       │                            │         │      │
│   wp i18n              Poedit/Loco      msgfmt     │
│   make-pot                                          │
└─────────────────────────────────────────────────────┘

Translation plugins

Tools that enable translation management within WordPress admin; Loco Translate allows in-dashboard editing, WPML handles multilingual sites, while Poedit is a desktop application for PO files.

┌─────────────────────────────────────────────────────────────┐
│ Plugin/Tool         │ Use Case                              │
├─────────────────────┼───────────────────────────────────────┤
│ Loco Translate      │ In-admin PO/MO editing, POT creation  │
│ Poedit (Desktop)    │ Professional translation workflow     │
│ WPML                │ Multilingual content (pages/posts)    │
│ Polylang            │ Free multilingual alternative         │
│ TranslatePress      │ Visual front-end translation          │
│ WP-CLI i18n         │ CLI-based POT generation              │
└─────────────────────┴───────────────────────────────────────┘