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 <script> 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 │
└─────────────────────┴───────────────────────────────────────┘