Back to Articles
22 min read

WordPress Architecture & Theme Development: The Complete Fundamentals Guide

A comprehensive technical deep dive into how WordPress functions under the hood. This guide covers the essential bridge between WordPress Core architecture—including database schemas and the loading sequence—and the practical foundations of building custom themes using the Template Hierarchy, `functions.php`, and asset management.

WordPress Core Fundamentals

WordPress Architecture


WordPress Installation Methods

WordPress can be installed via the famous 5-minute web installer, WP-CLI (wp core download && wp core install), one-click hosting installers, Docker containers, or Composer-based setups like Bedrock for modern development workflows.

# WP-CLI Installation (preferred for developers) wp core download wp config create --dbname=wpdb --dbuser=root --dbpass=secret wp db create wp core install --url=localhost --title="Dev Site" \ --admin_user=admin --admin_email=dev@test.com

WordPress File Structure

WordPress follows a specific directory hierarchy separating core files (never modify) from customizable content; understanding this structure is fundamental for theme/plugin development.

wordpress/
├── wp-admin/           # Admin dashboard (DO NOT EDIT)
├── wp-includes/        # Core libraries (DO NOT EDIT)
├── wp-content/         # YOUR WORK GOES HERE
│   ├── themes/         # Theme files
│   ├── plugins/        # Plugin files
│   ├── uploads/        # Media uploads
│   └── mu-plugins/     # Must-use plugins
├── wp-config.php       # Configuration
├── index.php           # Entry point
└── .htaccess           # Server rules

WordPress Database Schema

WordPress uses 12 default tables with a configurable prefix; core tables store posts, metadata, users, options, and taxonomy relationships, following an EAV (Entity-Attribute-Value) pattern for flexible metadata.

┌─────────────────────────────────────────────────────────────┐
│                WordPress Database Schema                     │
├─────────────────────────────────────────────────────────────┤
│  wp_posts         │ All content (posts, pages, CPT, menus)  │
│  wp_postmeta      │ Post metadata (key-value pairs)         │
│  wp_users         │ User accounts                           │
│  wp_usermeta      │ User metadata                           │
│  wp_options       │ Site settings                           │
│  wp_terms         │ Taxonomy terms                          │
│  wp_term_taxonomy │ Term-taxonomy relationships             │
│  wp_term_relation │ Object-term relationships               │
│  wp_comments      │ Comments                                │
│  wp_commentmeta   │ Comment metadata                        │
│  wp_links         │ Blogroll (deprecated)                   │
└─────────────────────────────────────────────────────────────┘

wp-config.php Configuration

The main configuration file containing database credentials, security keys, debug settings, and custom constants; loaded early in WordPress bootstrap and should be secured with 400/440 permissions.

<?php // Essential wp-config.php settings define('DB_NAME', 'wordpress'); define('DB_USER', 'dbuser'); define('DB_PASSWORD', 'secure_password'); define('DB_HOST', 'localhost'); define('DB_CHARSET', 'utf8mb4'); // Security keys (get from https://api.wordpress.org/secret-key/1.1/salt/) define('AUTH_KEY', 'unique-phrase-here'); // Environment-specific define('WP_DEBUG', true); define('WP_DEBUG_LOG', true); define('WP_DEBUG_DISPLAY', false); define('WP_ENVIRONMENT_TYPE', 'development'); // local|development|staging|production

WordPress Loading Sequence

WordPress bootstraps through a specific sequence: index.php → wp-blog-header.php → wp-load.php → wp-config.php → wp-settings.php, loading core, plugins, themes, then executing the main query and template.

┌─────────────────────────────────────────────────────────────┐
│              WordPress Loading Sequence                      │
├─────────────────────────────────────────────────────────────┤
│  1. index.php                    Entry point                │
│  2. wp-blog-header.php           Load WP & template         │
│  3. wp-load.php                  Find wp-config.php         │
│  4. wp-config.php                Configuration              │
│  5. wp-settings.php              Core bootstrap             │
│     ├── Load core files                                     │
│     ├── Register defaults                                   │
│     ├── Load mu-plugins          ← 'muplugins_loaded'       │
│     ├── Load plugins             ← 'plugins_loaded'         │
│     ├── Load theme               ← 'after_setup_theme'      │
│     └── Init                     ← 'init'                   │
│  6. wp()                         Main query                 │
│  7. template-loader.php          Select template            │
└─────────────────────────────────────────────────────────────┘

WordPress Core Files Overview

Key core files include wp-settings.php (bootstrap), wp-includes/plugin.php (hooks system), wp-includes/post.php (post functions), wp-includes/query.php (WP_Query), and wp-includes/class-wp.php (main WordPress class).

wp-includes/
├── class-wp.php           # Main WP class (query parsing)
├── class-wp-query.php     # WP_Query class
├── class-wp-post.php      # WP_Post object
├── plugin.php             # Hooks API (add_action/filter)
├── post.php               # Post functions
├── taxonomy.php           # Taxonomy functions
├── user.php               # User functions
├── option.php             # Options API
├── formatting.php         # Sanitization/escaping
└── rest-api/              # REST API classes

.htaccess Configuration

Apache server configuration file managing pretty permalinks, security rules, and redirects; WordPress writes rewrite rules automatically, but developers often add custom security headers and caching rules.

# WordPress default + security enhancements # BEGIN WordPress <IfModule mod_rewrite.c> RewriteEngine On RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] RewriteBase / RewriteRule ^index\.php$ - [L] RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . /index.php [L] </IfModule> # END WordPress # Security hardening <Files wp-config.php> Order Allow,Deny Deny from all </Files>

WordPress Constants

WordPress defines numerous constants for paths, URLs, and configuration; knowing these prevents hardcoding paths and enables portable, secure code across different environments.

<?php // Path Constants (filesystem) ABSPATH // /var/www/html/wordpress/ WP_CONTENT_DIR // /var/www/html/wordpress/wp-content WP_PLUGIN_DIR // /var/www/html/wordpress/wp-content/plugins WPINC // wp-includes // URL Constants WP_CONTENT_URL // https://example.com/wp-content WP_PLUGIN_URL // https://example.com/wp-content/plugins // Configuration Constants WP_DEBUG // true/false WP_DEBUG_LOG // true/false or path AUTOSAVE_INTERVAL // 60 (seconds) WP_POST_REVISIONS // true/false or number DISALLOW_FILE_EDIT // Disable theme/plugin editor

Debugging with WP_DEBUG

WordPress's built-in debugging system that displays PHP errors and notices; essential during development, should be disabled in production. Combine with WP_DEBUG_LOG to write errors to /wp-content/debug.log.

<?php // Development debugging setup in wp-config.php define('WP_DEBUG', true); // Enable debugging define('WP_DEBUG_LOG', true); // Log to /wp-content/debug.log define('WP_DEBUG_DISPLAY', false); // Hide errors on screen define('SCRIPT_DEBUG', true); // Use non-minified core scripts define('SAVEQUERIES', true); // Save DB queries for analysis // In your code - proper debugging if (WP_DEBUG) { error_log('Debug: ' . print_r($variable, true)); }

Query Monitor Plugin Usage

Query Monitor is the essential WordPress debugging plugin showing database queries, hooks fired, HTTP requests, PHP errors, and performance metrics; far superior to var_dump() debugging and works within the admin bar.

┌─────────────────────────────────────────────────────────────┐
│            Query Monitor Panel Overview                      │
├─────────────────────────────────────────────────────────────┤
│  Queries     │ 45 queries in 0.0823s (slow queries flagged) │
│  Hooks       │ 847 actions, 156 filters executed            │
│  Theme       │ Template: single-post.php, hierarchy shown   │
│  PHP Errors  │ 2 notices, 1 deprecated warning              │
│  HTTP API    │ 3 external requests (timing shown)           │
│  Caps        │ Current user capabilities checked            │
│  Environment │ PHP 8.2, MySQL 8.0, Memory: 42MB/256MB       │
└─────────────────────────────────────────────────────────────┘
Install: wp plugin install query-monitor --activate

WordPress Core Concepts

Posts vs Pages

Posts are time-based, chronological content entries (blog posts) with categories/tags, appearing in RSS feeds; Pages are hierarchical, static content (About, Contact) without taxonomy support—both stored in wp_posts table with different post_type values.

┌─────────────────────────────────────────────────────────────┐
│                  Posts vs Pages                              │
├─────────────────────┬───────────────────────────────────────┤
│       POSTS         │           PAGES                       │
├─────────────────────┼───────────────────────────────────────┤
│ Chronological       │ Static/Hierarchical                   │
│ Categories & Tags   │ No taxonomies (default)               │
│ In RSS feeds        │ Not in feeds                          │
│ post_type = 'post'  │ post_type = 'page'                    │
│ Author archives     │ No archives                           │
│ Blog content        │ About, Contact, Services              │
└─────────────────────┴───────────────────────────────────────┘

Post Types

WordPress content types stored in wp_posts table; includes built-in types (post, page, attachment, revision, nav_menu_item) and custom post types (CPT) for products, events, portfolios—extending WordPress beyond blogging.

<?php // Register Custom Post Type add_action('init', function() { register_post_type('product', [ 'labels' => [ 'name' => 'Products', 'singular_name' => 'Product', ], 'public' => true, 'has_archive' => true, 'menu_icon' => 'dashicons-cart', 'supports' => ['title', 'editor', 'thumbnail', 'custom-fields'], 'rewrite' => ['slug' => 'products'], 'show_in_rest' => true, // Enable Gutenberg & REST API ]); });

Taxonomies (Categories/Tags)

Taxonomies group and classify content; Categories are hierarchical (parent/child), Tags are flat—both are built-in taxonomies, but custom taxonomies can be registered for CPTs (e.g., "Genre" for Books, "Brand" for Products).

<?php // Built-in: category (hierarchical), post_tag (flat) // Register Custom Taxonomy add_action('init', function() { register_taxonomy('genre', ['book'], [ 'labels' => ['name' => 'Genres', 'singular_name' => 'Genre'], 'hierarchical' => true, // Like categories (false = like tags) 'public' => true, 'show_in_rest' => true, 'rewrite' => ['slug' => 'genre'], ]); }); // Usage wp_set_object_terms($post_id, ['fiction', 'thriller'], 'genre');

Users and Roles

WordPress has a role-based access control system with 5 default roles (Super Admin, Administrator, Editor, Author, Contributor, Subscriber), each with specific capabilities; custom roles and capabilities can be added for granular permissions.

<?php // Default Role Hierarchy /* ┌─────────────────────────────────────────────────────────────┐ │ Super Admin → Administrator → Editor → Author → Contributor → Subscriber │ └─────────────────────────────────────────────────────────────┘ */ // Add custom role add_role('shop_manager', 'Shop Manager', [ 'read' => true, 'edit_products' => true, 'manage_orders' => true, ]); // Check capability if (current_user_can('edit_posts')) { // User can edit posts } // Add capability to existing role $role = get_role('editor'); $role->add_cap('manage_options');

Comments System

Built-in discussion system storing comments in wp_comments and wp_commentmeta tables; supports threading, moderation, spam protection (Akismet), and avatars (Gravatars). Can be disabled globally or per-post.

<?php // Check if comments are open if (comments_open()) { comments_template(); // Load comments.php } // Custom comment query $comments = get_comments([ 'post_id' => get_the_ID(), 'status' => 'approve', 'order' => 'ASC', ]); // Programmatically add comment wp_insert_comment([ 'comment_post_ID' => $post_id, 'comment_content' => 'Great article!', 'user_id' => get_current_user_id(), 'comment_approved' => 1, ]);

Media Library

WordPress media management system storing uploads in /wp-content/uploads/ (organized by year/month) with metadata in wp_posts (attachment post type) and wp_postmeta; automatically generates multiple image sizes defined by themes.

<?php // Add custom image sizes in theme add_action('after_setup_theme', function() { add_theme_support('post-thumbnails'); add_image_size('card-thumb', 350, 200, true); // Hard crop add_image_size('hero', 1920, 600, ['center', 'center']); }); // Get attachment data $image_id = get_post_thumbnail_id($post_id); $image_url = wp_get_attachment_image_url($image_id, 'card-thumb'); $image_srcset = wp_get_attachment_image_srcset($image_id, 'large'); // Upload structure: /wp-content/uploads/2024/01/image.jpg

Permalinks Structure

URL rewriting system transforming ?p=123 into SEO-friendly URLs; configured in Settings → Permalinks, stored in wp_options, and rules written to .htaccess. Custom structures use tags like %postname%, %category%.

┌─────────────────────────────────────────────────────────────┐
│             Permalink Structure Options                      │
├─────────────────────────────────────────────────────────────┤
│  Plain        │ ?p=123                                      │
│  Day/Name     │ /2024/01/15/sample-post/                   │
│  Month/Name   │ /2024/01/sample-post/                      │
│  Post name    │ /sample-post/              ← Recommended   │
│  Custom       │ /blog/%category%/%postname%/               │
├─────────────────────────────────────────────────────────────┤
│  Available tags: %year% %monthnum% %day% %hour%            │
│  %minute% %second% %postname% %post_id% %category% %author%│
└─────────────────────────────────────────────────────────────┘

// Add custom permalink for CPT
'rewrite' => ['slug' => 'products', 'with_front' => false]

WordPress Multisite Basics

Network installation allowing multiple sites from single WordPress codebase; sites share core/plugins/themes but have separate content. Uses subdomain (site1.example.com) or subdirectory (example.com/site1) structure with additional database tables.

<?php // Enable Multisite in wp-config.php (before install) define('WP_ALLOW_MULTISITE', true); // After network setup, add these: define('MULTISITE', true); define('SUBDOMAIN_INSTALL', false); // true for subdomains define('DOMAIN_CURRENT_SITE', 'example.com'); define('PATH_CURRENT_SITE', '/'); define('SITE_ID_CURRENT_SITE', 1); define('BLOG_ID_CURRENT_SITE', 1); // Additional tables created: // wp_blogs, wp_site, wp_sitemeta, wp_2_posts, wp_2_options, etc. // Switch between sites switch_to_blog(2); // Do something on site 2 restore_current_blog();

This covers Phase 1 and Phase 2. Would you like me to continue with the remaining phases covering Theme Development, Plugin Development, and Advanced Topics?


Theme Basics

Theme folder structure

A WordPress theme requires a minimum of two files (style.css and index.php) placed in a folder under /wp-content/themes/. A well-organized theme includes additional directories for assets, template parts, and includes.

my-theme/
├── style.css          (required)
├── index.php          (required)
├── functions.php
├── screenshot.png
├── header.php
├── footer.php
├── sidebar.php
├── /assets/
│   ├── /css/
│   ├── /js/
│   └── /images/
├── /template-parts/
├── /inc/
└── /templates/

style.css header requirements

The style.css file must contain a header comment block with specific metadata fields that WordPress parses to identify and display the theme in the admin panel; Theme Name is the only required field, but including version, author, and description is best practice.

/* Theme Name: My Awesome Theme Theme URI: https://example.com/theme Author: Your Name Author URI: https://example.com Description: A custom WordPress theme Version: 1.0.0 License: GPL-2.0-or-later License URI: https://www.gnu.org/licenses/gpl-2.0.html Text Domain: my-awesome-theme Tags: blog, responsive, custom-header Requires at least: 6.0 Requires PHP: 8.0 */

index.php requirements

The index.php is the ultimate fallback template in WordPress's template hierarchy and must exist even if empty; it serves as the catch-all template when no more specific template file matches the requested content type.

<?php get_header(); ?> <main id="primary" class="site-main"> <?php if ( have_posts() ) : while ( have_posts() ) : the_post(); the_title( '<h2>', '</h2>' ); the_content(); endwhile; else : echo '<p>No content found.</p>'; endif; ?> </main> <?php get_sidebar(); get_footer();

Screenshot.png specifications

The screenshot.png displays in the Appearance > Themes admin screen and should be 1200×900 pixels (4:3 ratio) in PNG or JPG format; keep file size under 1MB for performance, and the image should accurately represent the theme's design.

┌─────────────────────────────┐
│      screenshot.png         │
│                             │
│   Dimensions: 1200 x 900    │
│   Ratio: 4:3                │
│   Format: PNG (preferred)   │
│   Max size: ~1MB            │
│                             │
└─────────────────────────────┘

Theme activation process

When you activate a theme, WordPress fires the after_switch_theme hook, loads the theme's functions.php, registers theme features, and updates the template and stylesheet options in the database; use this hook to set default options or run one-time setup tasks.

// In functions.php function mytheme_activation_setup() { // Set default options update_option( 'mytheme_installed', time() ); // Create default pages, menus, etc. flush_rewrite_rules(); } add_action( 'after_switch_theme', 'mytheme_activation_setup' ); // Deactivation cleanup function mytheme_deactivation_cleanup() { // Optional: clean up transients, etc. } add_action( 'switch_theme', 'mytheme_deactivation_cleanup' );

Theme metadata

Theme metadata includes all information defined in the style.css header and can be retrieved programmatically using wp_get_theme(); this includes name, version, author, description, tags, and requirements, useful for displaying theme info or version checks.

$theme = wp_get_theme(); echo $theme->get( 'Name' ); // "My Awesome Theme" echo $theme->get( 'Version' ); // "1.0.0" echo $theme->get( 'Author' ); // "Your Name" echo $theme->get( 'Description' ); // Theme description echo $theme->get( 'TextDomain' ); // "my-awesome-theme" echo $theme->get( 'ThemeURI' ); // Theme URL // Check parent theme (for child themes) $parent = $theme->parent();

Template Hierarchy

Template hierarchy diagram

WordPress uses a specific decision tree to determine which template file to use for any given request, starting from most specific to least specific, always falling back to index.php if no other template matches.

Request Type Template Hierarchy (most → least specific) ───────────────────────────────────────────────────────────────────── SINGLE POST: singular.php ←─┬─ single.php ←── single-{post-type}-{slug}.php │ ↑ │ single-{post-type}.php └─ page.php ←── page-{slug}.php page-{id}.php {custom-template}.php ARCHIVE: archive.php ←── category.php ←── category-{slug}.php ↑ ↑ tag.php category-{id}.php author.php ←── author-{nicename}.php ↑ ↑ date.php author-{id}.php FALLBACK: ──────────────────────────────────────────────→ index.php

index.php fallback

The index.php serves as the universal fallback template that WordPress uses when no other template in the hierarchy matches; every theme must have this file, and it will display any content type if specific templates don't exist.

┌──────────────────────────────────────────────────────────┐
│                   TEMPLATE RESOLUTION                     │
├──────────────────────────────────────────────────────────┤
│                                                          │
│  Request: /category/news/                                │
│                                                          │
│  WordPress checks:                                       │
│  1. category-news.php      ✗ Not found                  │
│  2. category-5.php         ✗ Not found                  │
│  3. category.php           ✗ Not found                  │
│  4. archive.php            ✗ Not found                  │
│  5. index.php              ✓ USED (fallback)            │
│                                                          │
└──────────────────────────────────────────────────────────┘

single.php

The single.php template displays individual blog posts (post type "post" by default); you can create more specific templates like single-{post-type}.php or single-{post-type}-{slug}.php for custom post types or specific posts.

<?php get_header(); ?> <article id="post-<?php the_ID(); ?>" <?php post_class(); ?>> <header class="entry-header"> <?php the_title( '<h1>', '</h1>' ); ?> <div class="meta"> Posted on <?php echo get_the_date(); ?> by <?php the_author(); ?> </div> </header> <div class="entry-content"> <?php the_content(); ?> </div> <?php // Post navigation the_post_navigation(); // Comments if ( comments_open() ) { comments_template(); } ?> </article> <?php get_footer(); ?>

page.php

The page.php template handles static pages (post type "page"); WordPress checks for {custom-template}.phppage-{slug}.phppage-{id}.phppage.phpsingular.phpindex.php in that order.

<?php /** * Template for displaying pages */ get_header(); ?> <main id="primary" class="site-main"> <?php while ( have_posts() ) : the_post(); ?> <article id="page-<?php the_ID(); ?>" <?php post_class(); ?>> <h1><?php the_title(); ?></h1> <div class="page-content"> <?php the_content(); // Pagination for paginated pages wp_link_pages(); ?> </div> </article> <?php endwhile; ?> </main> <?php get_footer(); ?>

archive.php

The archive.php template is the generic fallback for all archive pages (category, tag, date, author, custom taxonomy, post type archives); it displays a list of posts matching the archive criteria and serves as the base when more specific archive templates don't exist.

<?php get_header(); ?> <main id="primary" class="archive-page"> <header class="archive-header"> <?php the_archive_title( '<h1>', '</h1>' ); the_archive_description( '<div class="archive-description">', '</div>' ); ?> </header> <?php if ( have_posts() ) : ?> <div class="posts-grid"> <?php while ( have_posts() ) : the_post(); get_template_part( 'template-parts/content', get_post_type() ); endwhile; the_posts_pagination(); ?> </div> <?php else : ?> <p>No posts found.</p> <?php endif; ?> </main> <?php get_footer(); ?>

category.php

The category.php template specifically handles category archive pages; WordPress first checks for category-{slug}.php, then category-{id}.php, then category.php before falling back to archive.php, allowing granular control over different category displays.

<?php /** * Category Archive Template * Hierarchy: category-{slug}.php → category-{id}.php → category.php */ get_header(); $category = get_queried_object(); ?> <main class="category-archive"> <header> <h1>Category: <?php single_cat_title(); ?></h1> <?php echo category_description(); ?> <!-- Show parent/child relationship --> <?php if ( $category->parent ) : ?> <p>Parent: <?php echo get_cat_name( $category->parent ); ?></p> <?php endif; ?> </header> <?php while ( have_posts() ) : the_post(); get_template_part( 'template-parts/content', 'archive' ); endwhile; the_posts_navigation(); ?> </main> <?php get_footer(); ?>

tag.php

The tag.php template handles tag archive pages; the hierarchy follows tag-{slug}.phptag-{id}.phptag.phparchive.phpindex.php, allowing you to customize display for specific tags or all tags generally.

<?php get_header(); ?> <main class="tag-archive"> <header class="tag-header"> <h1>Posts tagged: <?php single_tag_title(); ?></h1> <?php echo tag_description(); ?> </header> <?php if ( have_posts() ) : ?> <ul class="tagged-posts"> <?php while ( have_posts() ) : the_post(); ?> <li> <a href="<?php the_permalink(); ?>"><?php the_title(); ?></a> <span class="date"><?php echo get_the_date(); ?></span> </li> <?php endwhile; ?> </ul> <?php the_posts_pagination(); ?> <?php endif; ?> </main> <?php get_footer(); ?>

taxonomy.php

The taxonomy.php template handles custom taxonomy archives (not categories or tags); the hierarchy is taxonomy-{taxonomy}-{term}.phptaxonomy-{taxonomy}.phptaxonomy.phparchive.php for displaying custom taxonomy term archives.

<?php /** * Custom Taxonomy Archive * Example: taxonomy-genre.php for a "genre" taxonomy */ get_header(); $term = get_queried_object(); ?> <main class="taxonomy-archive taxonomy-<?php echo $term->taxonomy; ?>"> <header> <h1><?php single_term_title(); ?></h1> <p class="term-description"><?php echo term_description(); ?></p> <!-- Display term meta if any --> <?php $term_meta = get_term_meta( $term->term_id, 'custom_field', true ); if ( $term_meta ) echo '<p>' . esc_html( $term_meta ) . '</p>'; ?> </header> <?php while ( have_posts() ) : the_post(); get_template_part( 'template-parts/content', $term->taxonomy ); endwhile; the_posts_navigation(); ?> </main> <?php get_footer(); ?>

author.php

The author.php template displays posts by a specific author; hierarchy goes author-{nicename}.phpauthor-{id}.phpauthor.phparchive.php, and you can access author data using get_queried_object() or author template tags.

<?php get_header(); ?> <?php $author = get_queried_object(); ?> <main class="author-archive"> <header class="author-box"> <div class="author-avatar"> <?php echo get_avatar( $author->ID, 150 ); ?> </div> <div class="author-info"> <h1><?php echo $author->display_name; ?></h1> <p class="bio"><?php echo $author->description; ?></p> <p class="post-count"> <?php printf( '%d posts', count_user_posts( $author->ID ) ); ?> </p> </div> </header> <h2>Posts by <?php echo $author->display_name; ?></h2> <?php while ( have_posts() ) : the_post(); get_template_part( 'template-parts/content', 'archive' ); endwhile; ?> </main> <?php get_footer(); ?>

date.php

The date.php template handles all date-based archives (year, month, day); you can determine the date granularity using conditional tags like is_year(), is_month(), or is_day() to customize the display accordingly.

<?php get_header(); ?> <main class="date-archive"> <header> <h1> <?php if ( is_day() ) : printf( 'Daily Archives: %s', get_the_date() ); elseif ( is_month() ) : printf( 'Monthly Archives: %s', get_the_date( 'F Y' ) ); elseif ( is_year() ) : printf( 'Yearly Archives: %s', get_the_date( 'Y' ) ); endif; ?> </h1> </header> <?php if ( have_posts() ) : while ( have_posts() ) : the_post(); ?> <article> <h2><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h2> <time datetime="<?php echo get_the_date( 'c' ); ?>"> <?php echo get_the_date(); ?> </time> </article> <?php endwhile; the_posts_pagination(); endif; ?> </main> <?php get_footer(); ?>

search.php

The search.php template displays search results; the search query is available via get_search_query(), and this template should handle both cases where results are found and when no results match the search terms.

<?php get_header(); ?> <main class="search-results"> <header> <h1> <?php printf( 'Search Results for: "%s"', get_search_query() ); ?> </h1> </header> <?php if ( have_posts() ) : ?> <p class="results-count"> <?php printf( 'Found %d results', $wp_query->found_posts ); ?> </p> <?php while ( have_posts() ) : the_post(); ?> <article <?php post_class(); ?>> <h2><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h2> <p><?php echo wp_trim_words( get_the_excerpt(), 30 ); ?></p> <span class="post-type"><?php echo get_post_type(); ?></span> </article> <?php endwhile; the_posts_pagination(); ?> <?php else : ?> <p>No results found. Try different keywords.</p> <?php get_search_form(); ?> <?php endif; ?> </main> <?php get_footer(); ?>

404.php

The 404.php template displays when WordPress cannot find the requested content; it should provide helpful navigation options like search, popular posts, or category links to help users find what they're looking for.

<?php get_header(); ?> <main class="error-404"> <header> <h1>404 - Page Not Found</h1> <p>The page you're looking for doesn't exist or has been moved.</p> </header> <div class="error-content"> <section class="search-section"> <h2>Try Searching</h2> <?php get_search_form(); ?> </section> <section class="helpful-links"> <h2>Popular Pages</h2> <ul> <li><a href="<?php echo home_url(); ?>">Home</a></li> <li><a href="<?php echo get_permalink( get_option( 'page_for_posts' ) ); ?>">Blog</a></li> </ul> <h2>Categories</h2> <ul> <?php wp_list_categories( array( 'title_li' => '' ) ); ?> </ul> </section> </div> </main> <?php get_footer(); ?>

front-page.php

The front-page.php template takes precedence over all other templates when displaying the site's front page, regardless of the "Front page displays" setting in Settings → Reading; it's used for creating custom landing pages.

<?php /** * Front Page Template * Always used for site front regardless of Settings > Reading */ get_header(); ?> <main class="front-page"> <!-- Hero Section --> <section class="hero"> <h1><?php bloginfo( 'name' ); ?></h1> <p><?php bloginfo( 'description' ); ?></p> </section> <!-- Featured Content --> <section class="featured-posts"> <?php $featured = new WP_Query( array( 'posts_per_page' => 3, 'meta_key' => 'featured', 'meta_value' => '1', ) ); while ( $featured->have_posts() ) : $featured->the_post(); get_template_part( 'template-parts/content', 'featured' ); endwhile; wp_reset_postdata(); ?> </section> </main> <?php get_footer(); ?>

home.php

The home.php template displays the blog posts index page; when a static front page is set, home.php displays on the "Posts page" you specify, making it distinct from front-page.php which handles the actual front page.

┌────────────────────────────────────────────────────────────┐
│              Settings > Reading Configuration               │
├────────────────────────────────────────────────────────────┤
│                                                            │
│  "Your latest posts"          "A static page"              │
│  ─────────────────           ──────────────────            │
│  Front page: home.php        Front page: front-page.php    │
│                                    or page.php             │
│                              Posts page: home.php          │
│                                                            │
└────────────────────────────────────────────────────────────┘
<?php /** * Blog Posts Index Template (home.php) */ get_header(); ?> <main class="blog-index"> <header> <h1>Latest Posts</h1> </header> <?php if ( have_posts() ) : while ( have_posts() ) : the_post(); get_template_part( 'template-parts/content', 'excerpt' ); endwhile; the_posts_pagination( array( 'mid_size' => 2, 'prev_text' => '← Previous', 'next_text' => 'Next →', ) ); endif; ?> </main> <?php get_sidebar(); get_footer(); ?>

attachment.php

The attachment.php template displays individual attachment pages (images, PDFs, etc.); the hierarchy includes MIME-type specific templates like image.php, video.php, or even image-jpeg.php for granular control over attachment display.

<?php get_header(); ?> <?php while ( have_posts() ) : the_post(); ?> <article class="attachment-page"> <h1><?php the_title(); ?></h1> <div class="attachment-meta"> <p>Uploaded: <?php echo get_the_date(); ?></p> <p>Parent: <a href="<?php echo get_permalink( $post->post_parent ); ?>"> <?php echo get_the_title( $post->post_parent ); ?> </a> </p> </div> <div class="attachment-content"> <?php if ( wp_attachment_is_image() ) : ?> <figure> <?php echo wp_get_attachment_image( get_the_ID(), 'large' ); ?> <figcaption><?php the_excerpt(); ?></figcaption> </figure> <?php else : ?> <a href="<?php echo wp_get_attachment_url(); ?>" download> Download <?php the_title(); ?> </a> <?php endif; ?> </div> </article> <?php endwhile; ?> <?php get_footer(); ?>

singular.php

The singular.php template serves as a shared fallback for both single posts and pages; it's checked after single.php and page.php in their respective hierarchies, useful when you want identical layouts for both content types.

┌─────────────────────────────────────────────────────────┐
│                  SINGULAR HIERARCHY                      │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  Single Post Request         Page Request               │
│  ───────────────────        ─────────────               │
│  single-{type}-{slug}.php   {template}.php              │
│         ↓                        ↓                      │
│  single-{type}.php          page-{slug}.php             │
│         ↓                        ↓                      │
│  single.php                 page-{id}.php               │
│         ↓                        ↓                      │
│         └────────→ singular.php ←────────┘              │
│                        ↓                                │
│                    index.php                            │
│                                                         │
└─────────────────────────────────────────────────────────┘
<?php get_header(); ?> <main class="singular-content"> <?php while ( have_posts() ) : the_post(); ?> <article id="post-<?php the_ID(); ?>" <?php post_class(); ?>> <h1><?php the_title(); ?></h1> <?php the_content(); ?> </article> <?php if ( is_single() && comments_open() ) { comments_template(); } endwhile; ?> </main> <?php get_footer(); ?>

Core Template Files

header.php

The header.php template contains the opening HTML, <head> section, and opening <body> tag including navigation; it's loaded via get_header() and can have variations like header-landing.php loaded with get_header('landing').

<!DOCTYPE html> <html <?php language_attributes(); ?>> <head> <meta charset="<?php bloginfo( 'charset' ); ?>"> <meta name="viewport" content="width=device-width, initial-scale=1"> <?php wp_head(); ?> </head> <body <?php body_class(); ?>> <?php wp_body_open(); ?> <header id="masthead" class="site-header"> <div class="site-branding"> <?php if ( has_custom_logo() ) { the_custom_logo(); } else { echo '<h1><a href="' . home_url() . '">' . get_bloginfo('name') . '</a></h1>'; } ?> </div> <nav id="site-navigation"> <?php wp_nav_menu( array( 'theme_location' => 'primary', 'menu_id' => 'primary-menu', ) ); ?> </nav> </header>

footer.php

The footer.php template contains the closing markup, footer widgets, copyright information, and must include wp_footer() before the closing </body> tag; variations can be created like footer-minimal.php and loaded with get_footer('minimal').

<footer id="colophon" class="site-footer"> <div class="footer-widgets"> <?php if ( is_active_sidebar( 'footer-1' ) ) { dynamic_sidebar( 'footer-1' ); } ?> </div> <div class="site-info"> <p>&copy; <?php echo date('Y'); ?> <?php bloginfo('name'); ?></p> <nav class="footer-navigation"> <?php wp_nav_menu( array( 'theme_location' => 'footer', 'depth' => 1, ) ); ?> </nav> </div> </footer> <?php wp_footer(); ?> </body> </html>

sidebar.php

The sidebar.php template contains widget areas displayed via get_sidebar(); widgets are registered in functions.php with register_sidebar() and displayed using dynamic_sidebar(), with variations possible like sidebar-shop.php.

<?php /** * Sidebar Template * Load with: get_sidebar() or get_sidebar('shop') */ if ( ! is_active_sidebar( 'sidebar-1' ) ) { return; } ?> <aside id="secondary" class="widget-area" role="complementary"> <?php dynamic_sidebar( 'sidebar-1' ); ?> </aside> <!-- In functions.php: Register the sidebar --> <?php function mytheme_widgets_init() { register_sidebar( array( 'name' => 'Primary Sidebar', 'id' => 'sidebar-1', 'description' => 'Main sidebar widgets', 'before_widget' => '<section id="%1$s" class="widget %2$s">', 'after_widget' => '</section>', 'before_title' => '<h3 class="widget-title">', 'after_title' => '</h3>', ) ); } add_action( 'widgets_init', 'mytheme_widgets_init' );

comments.php

The comments.php template handles the display of comments and the comment form; it's loaded via comments_template() and includes the comments list, pagination, reply forms, and handles various states like closed comments or password-protected posts.

<?php /** * Comments Template */ // Don't load if accessed directly or password required if ( post_password_required() ) { return; } ?> <div id="comments" class="comments-area"> <?php if ( have_comments() ) : ?> <h2 class="comments-title"> <?php printf( '%d Comments', get_comments_number() ); ?> </h2> <ol class="comment-list"> <?php wp_list_comments( array( 'style' => 'ol', 'short_ping' => true, 'avatar_size' => 50, ) ); ?> </ol> <?php the_comments_pagination(); ?> <?php endif; ?> <?php if ( comments_open() ) : ?> <?php comment_form(); ?> <?php else : ?> <p class="no-comments">Comments are closed.</p> <?php endif; ?> </div>

searchform.php

The searchform.php template customizes the HTML output of get_search_form(); if this file doesn't exist, WordPress generates a default form, but creating it allows complete control over search form markup and styling.

<?php /** * Custom Search Form Template * Loaded by get_search_form() */ ?> <form role="search" method="get" class="search-form" action="<?php echo home_url( '/' ); ?>"> <label> <span class="screen-reader-text">Search for:</span> <input type="search" class="search-field" placeholder="Search..." value="<?php echo get_search_query(); ?>" name="s" required /> </label> <button type="submit" class="search-submit"> <span class="screen-reader-text">Search</span> <svg><!-- search icon --></svg> </button> </form>

Template parts

Template parts are reusable PHP snippets loaded via get_template_part() that promote DRY principles; they're typically stored in a template-parts/ directory and can accept dynamic suffixes and arguments for flexibility.

// Loading template parts get_template_part( 'template-parts/content' ); // Loads: template-parts/content.php get_template_part( 'template-parts/content', 'single' ); // Loads: template-parts/content-single.php get_template_part( 'template-parts/content', get_post_type() ); // Loads: template-parts/content-{post-type}.php // Passing arguments (WP 5.5+) get_template_part( 'template-parts/card', null, array( 'title' => 'Custom Title', 'show_image' => true, ) ); // In template-parts/card.php: $args = wp_parse_args( $args, array( 'title' => get_the_title(), 'show_image' => false, ) ); echo '<h3>' . $args['title'] . '</h3>';
template-parts/
├── content.php
├── content-single.php
├── content-page.php
├── content-archive.php
├── content-none.php
├── header/
│   ├── site-branding.php
│   └── navigation.php
└── cards/
    ├── card-post.php
    └── card-product.php

The Loop

Basic loop structure

The Loop is WordPress's core mechanism for iterating through and displaying posts; it checks if posts exist with have_posts(), advances the pointer with the_post(), and gives access to template tags like the_title() and the_content() inside the loop.

<?php // The Standard WordPress Loop if ( have_posts() ) : // Check if any posts exist while ( have_posts() ) : // Loop while posts exist the_post(); // Set up post data ?> <article id="post-<?php the_ID(); ?>" <?php post_class(); ?>> <h2><?php the_title(); ?></h2> <div class="meta"> <?php the_date(); ?> | <?php the_author(); ?> </div> <div class="content"> <?php the_content(); ?> </div> <div class="tags"> <?php the_tags( 'Tags: ', ', ' ); ?> </div> </article> <?php endwhile; the_posts_pagination(); // Pagination links else : echo '<p>No posts found.</p>'; endif; ?>

have_posts()

The have_posts() function checks if the current WordPress query has any remaining posts to iterate over; it returns true if posts exist in the loop and false when all posts have been processed or none exist, controlling loop continuation.

<?php // have_posts() returns boolean // Main query check if ( have_posts() ) { echo "Posts available: " . $wp_query->post_count; } // It's essentially a wrapper for: // $wp_query->have_posts() // Under the hood (simplified): function have_posts() { global $wp_query; return $wp_query->current_post + 1 < $wp_query->post_count; } // Common patterns: if ( have_posts() ) : while ( have_posts() ) : the_post(); // Display posts endwhile; else : // No posts found endif; // With custom query: $my_query = new WP_Query( $args ); if ( $my_query->have_posts() ) { while ( $my_query->have_posts() ) { $my_query->the_post(); } } ?>

the_post()

The the_post() function advances the loop to the next post and sets up all global post data ($post, $id, etc.), making template tags like the_title(), the_content(), and get_the_ID() work correctly; must be called inside the while loop.

<?php // the_post() sets up post data for template tags while ( have_posts() ) { the_post(); // MUST be called before using template tags // Now these work: the_ID(); // Echoes post ID the_title(); // Echoes post title the_content(); // Echoes post content the_excerpt(); // Echoes excerpt the_permalink(); // Echoes URL the_author(); // Echoes author name // "get_" versions return instead of echo: $id = get_the_ID(); $title = get_the_title(); $content = get_the_content(); } // Under the hood, the_post() does: // 1. $wp_query->the_post() // 2. Sets global $post // 3. Calls setup_postdata() // Manual setup (alternative): global $post; $post = get_post( 123 ); setup_postdata( $post ); // ... use template tags ... wp_reset_postdata(); ?>

WP_Query basics

WP_Query is the powerful class for creating custom queries independent of the main loop; it accepts numerous parameters to filter posts by type, taxonomy, meta, date, author, and more, returning a query object you iterate with its own methods.

<?php // WP_Query - Custom Queries $args = array( 'post_type' => 'post', // post, page, custom_type 'posts_per_page' => 10, // -1 for all 'post_status' => 'publish', 'orderby' => 'date', // date, title, rand, meta_value 'order' => 'DESC', // ASC or DESC // Category/Taxonomy 'category_name' => 'news', // by slug 'cat' => 5, // by ID 'tag' => 'featured', // Custom taxonomy 'tax_query' => array( array( 'taxonomy' => 'genre', 'field' => 'slug', 'terms' => array( 'action', 'comedy' ), ), ), // Meta/Custom fields 'meta_query' => array( array( 'key' => 'featured', 'value' => '1', 'compare' => '=', ), ), // Date 'date_query' => array( array( 'after' => '2024-01-01' ), ), ); $custom_query = new WP_Query( $args ); if ( $custom_query->have_posts() ) : while ( $custom_query->have_posts() ) : $custom_query->the_post(); the_title( '<h2>', '</h2>' ); endwhile; endif; wp_reset_postdata(); // IMPORTANT: Reset after custom query ?>

Loop within loops

Nested loops require using separate WP_Query instances and properly resetting post data after each inner loop with wp_reset_postdata(); never nest using the global $wp_query directly as it will corrupt the outer loop's state.

<?php // Nested Loops - Multiple queries // Main Loop (categories) $categories = get_categories(); foreach ( $categories as $category ) : ?> <section class="category-section"> <h2><?php echo $category->name; ?></h2> <?php // Inner Loop - Posts per category $cat_query = new WP_Query( array( 'cat' => $category->term_id, 'posts_per_page' => 3, ) ); if ( $cat_query->have_posts() ) : while ( $cat_query->have_posts() ) : $cat_query->the_post(); ?> <article> <h3><?php the_title(); ?></h3> <?php // Even deeper: Related posts $related = new WP_Query( array( 'post__not_in' => array( get_the_ID() ), 'posts_per_page' => 2, 'cat' => $category->term_id, ) ); while ( $related->have_posts() ) : $related->the_post(); echo '<span>' . get_the_title() . '</span>'; endwhile; wp_reset_postdata(); // Reset inner-inner ?> </article> <?php endwhile; endif; wp_reset_postdata(); // Reset inner ?> </section> <?php endforeach; ?>

Resetting post data

After using WP_Query or modifying the global $post, you must call wp_reset_postdata() to restore the original query's post data; wp_reset_query() is the nuclear option that resets both $post and $wp_query and should only be used after query_posts().

<?php // ✅ wp_reset_postdata() - Use after WP_Query or setup_postdata() $featured = new WP_Query( array( 'posts_per_page' => 5 ) ); while ( $featured->have_posts() ) : $featured->the_post(); the_title(); endwhile; wp_reset_postdata(); // Restores global $post to main query // ✅ Now main query works correctly again if ( have_posts() ) : // Uses original main query endif; // ────────────────────────────────────────────── // ⚠️ wp_reset_query() - Only after query_posts() (deprecated pattern) query_posts( 'posts_per_page=5' ); // Modifies main query (avoid!) while ( have_posts() ) : the_post(); the_title(); endwhile; wp_reset_query(); // Resets $wp_query AND $post // ────────────────────────────────────────────── // 📊 When to use what: // // Situation → Solution // ───────────────────────────────────────────── // After WP_Query → wp_reset_postdata() // After setup_postdata() → wp_reset_postdata() // After query_posts() → wp_reset_query() // After get_posts() + foreach → wp_reset_postdata() ?> <?php // get_posts() example $posts = get_posts( array( 'numberposts' => 5 ) ); foreach ( $posts as $post ) : setup_postdata( $post ); the_title(); endforeach; wp_reset_postdata(); ?>

This covers all the fundamental concepts for WordPress theme development. Want me to continue with Plugin Development topics next?

Essential Template Tags

the_title()

Outputs the current post/page title within The Loop. It's the most basic template tag for displaying headlines. Use get_the_title() to return instead of echo.

<h1><?php the_title(); ?></h1> <!-- With before/after params --> <?php the_title('<h2 class="entry-title">', '</h2>'); ?>

the_content()

Displays the full post content including any formatting, shortcodes, and embeds. Must be used inside The Loop. Applies the_content filter which processes shortcodes and oEmbed.

<article> <?php the_content('Read more...'); ?> </article> <!-- 'Read more...' = text shown for <!--more--> tag -->

the_excerpt()

Outputs a trimmed version of post content (default 55 words) for archive/listing pages. Auto-generated from content or uses manual excerpt field. Strips all HTML tags.

<div class="summary"> <?php the_excerpt(); ?> </div> <!-- Filter to change length: --> <?php // add_filter('excerpt_length', fn() => 30); ?>

the_permalink()

Echoes the full URL (permanent link) to the current post. Essential for linking post titles and "read more" buttons to single post views.

<a href="<?php the_permalink(); ?>"> <?php the_title(); ?> </a>

the_post_thumbnail()

Displays the featured image of the current post. Requires theme support enabled. Accepts size parameter (thumbnail, medium, large, full, or custom).

<?php if (has_post_thumbnail()): ?> <?php the_post_thumbnail('large', ['class' => 'featured-img']); ?> <?php endif; ?>

the_author()

Outputs the display name of the post author. For linked author archives, use the_author_posts_link() instead.

<span class="author">Written by: <?php the_author(); ?></span> <!-- Or with link to author archive --> <span class="author"><?php the_author_posts_link(); ?></span>

the_date()

Displays the post publication date. Important quirk: only shows once per day in loops (to avoid repetition). Use get_the_date() for consistent output.

<time datetime="<?php echo get_the_date('c'); ?>"> <?php echo get_the_date('F j, Y'); ?> </time> <!-- Output: October 15, 2024 -->

the_category()

Outputs a formatted list of categories assigned to the current post, with links to category archives.

<div class="categories"> <?php the_category(', '); ?> <!-- separator between categories --> </div> <!-- Output: <a href="...">Tech</a>, <a href="...">News</a> -->

the_tags()

Displays post tags with customizable before text, separator, and after text. Returns nothing if no tags assigned.

<?php the_tags('Tags: ', ', ', '<br>'); ?> <!-- Output: Tags: PHP, WordPress, Development<br> -->

get_template_part()

Includes a reusable template file, enabling DRY (Don't Repeat Yourself) code. WordPress's way of modular templating with optional name variants and data passing.

// Loads template-parts/content.php get_template_part('template-parts/content'); // Loads template-parts/content-video.php for video format get_template_part('template-parts/content', get_post_format()); // Pass data (WP 5.5+) get_template_part('template-parts/card', null, ['title' => 'Hello']);
┌─────────────────────────────────────────────────────────────┐
│  TEMPLATE PART LOADING HIERARCHY                            │
├─────────────────────────────────────────────────────────────┤
│  get_template_part('content', 'video')                      │
│                     │                                       │
│                     ▼                                       │
│  1. child-theme/content-video.php   ─── Found? ──► Load    │
│                     │ No                                    │
│                     ▼                                       │
│  2. parent-theme/content-video.php  ─── Found? ──► Load    │
│                     │ No                                    │
│                     ▼                                       │
│  3. child-theme/content.php         ─── Found? ──► Load    │
│                     │ No                                    │
│                     ▼                                       │
│  4. parent-theme/content.php        ─── Found? ──► Load    │
└─────────────────────────────────────────────────────────────┘

functions.php Basics

Theme Setup Function

A centralized function that initializes all theme features and capabilities. Best practice is to wrap setup code in a dedicated function hooked to after_setup_theme for proper timing.

function mytheme_setup() { // All theme setup code goes here add_theme_support('title-tag'); add_theme_support('post-thumbnails'); register_nav_menus(['primary' => 'Main Menu']); } add_action('after_setup_theme', 'mytheme_setup');

after_setup_theme Hook

Fires after the theme is loaded but before any output, making it the proper place to register theme features. Runs on every page load after functions.php is loaded.

┌──────────────────────────────────────────────────────────────┐
│  WORDPRESS INITIALIZATION SEQUENCE                           │
├──────────────────────────────────────────────────────────────┤
│  1. muplugins_loaded                                         │
│  2. plugins_loaded                                           │
│  3. setup_theme                                              │
│  4. after_setup_theme  ◄─── Register theme features here     │
│  5. init               ◄─── Register post types, taxonomies  │
│  6. wp_loaded                                                │
│  7. template_redirect                                        │
└──────────────────────────────────────────────────────────────┘

add_theme_support()

Enables built-in WordPress features for your theme. Each feature unlocks specific functionality in the editor and frontend. Called during theme setup.

add_theme_support('post-thumbnails'); // Featured images add_theme_support('title-tag'); // Dynamic <title> add_theme_support('html5', ['comment-list', 'search-form']); add_theme_support('custom-logo'); add_theme_support('post-formats', ['video', 'gallery']); add_theme_support('automatic-feed-links'); add_theme_support('editor-styles');

Post Thumbnails

Enables featured image functionality and allows defining custom image sizes for responsive design and performance optimization.

// Enable support add_theme_support('post-thumbnails'); // Add custom sizes add_image_size('card-thumb', 350, 200, true); // hard crop add_image_size('hero', 1920, 600, true); // Usage in template the_post_thumbnail('card-thumb');

Allows site owners to upload a logo through the Customizer. Define dimensions and flexibility for responsive behavior.

add_theme_support('custom-logo', [ 'height' => 100, 'width' => 400, 'flex-height' => true, 'flex-width' => true, ]); // Display in header.php if (has_custom_logo()) { the_custom_logo(); } else { echo '<h1>' . get_bloginfo('name') . '</h1>'; }

Title Tag

Lets WordPress manage the <title> tag dynamically instead of hardcoding. WordPress generates appropriate titles based on current page context.

// In functions.php add_theme_support('title-tag'); // In header.php - NO manual <title> tag needed! // WordPress auto-injects via wp_head()
┌────────────────────────────────────────────────────────┐
│  GENERATED TITLES BY CONTEXT                           │
├────────────────────────────────────────────────────────┤
│  Home:     Site Name – Tagline                         │
│  Single:   Post Title – Site Name                      │
│  Archive:  Category: Name – Site Name                  │
│  Search:   Search Results for "query" – Site Name      │
└────────────────────────────────────────────────────────┘

HTML5 Support

Enables semantic HTML5 markup for core WordPress elements instead of legacy XHTML output, improving accessibility and modern standards compliance.

add_theme_support('html5', [ 'comment-list', 'comment-form', 'search-form', 'gallery', 'caption', 'style', 'script', // Removes type="text/javascript" ]);

Post Formats

Provides standardized formatting options for different content types (video, audio, gallery, etc.), allowing themes to style content differently based on format.

add_theme_support('post-formats', [ 'aside', 'gallery', 'image', 'video', 'audio', 'link', 'quote', 'status' ]); // In template $format = get_post_format() ?: 'standard'; get_template_part('template-parts/content', $format);

Enqueuing Assets

wp_enqueue_style()

The proper way to add CSS files to your theme. Handles dependencies, prevents duplicates, and allows child themes to dequeue/override styles.

wp_enqueue_style( 'mytheme-style', // Handle (unique ID) get_stylesheet_uri(), // URL to CSS file ['normalize'], // Dependencies '1.0.0', // Version 'all' // Media );

wp_enqueue_script()

Properly loads JavaScript files with dependency management, footer placement, and deferred loading support. Essential for performance and conflict prevention.

wp_enqueue_script( 'mytheme-main', // Handle get_template_directory_uri() . '/js/app.js', // URL ['jquery'], // Dependencies '1.0.0', // Version true // In footer ); // WP 6.3+ strategy parameter wp_enqueue_script('app', $url, [], '1.0', ['strategy' => 'defer']);

wp_register_style()

Registers a stylesheet without immediately loading it. Useful for conditional loading or when styles are dependencies for other stylesheets.

// Register (doesn't load yet) wp_register_style('slick-carousel', $url, [], '1.8.1'); // Enqueue later when needed if (is_page_template('template-gallery.php')) { wp_enqueue_style('slick-carousel'); }

wp_register_script()

Registers scripts for later conditional enqueuing. Common pattern for third-party libraries that are only needed on specific pages.

// Register once wp_register_script('google-maps', 'https://maps.googleapis.com/maps/api/js?key=XXX', [], null, true ); // Enqueue only on contact page if (is_page('contact')) { wp_enqueue_script('google-maps'); }

wp_localize_script()

Passes PHP data (ajax URLs, translations, settings) to JavaScript. Creates a global JS object accessible in your scripts.

wp_enqueue_script('mytheme-ajax', get_template_directory_uri() . '/js/ajax.js'); wp_localize_script('mytheme-ajax', 'myThemeData', [ 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('mytheme_nonce'), 'homeUrl' => home_url(), 'i18n' => ['loading' => __('Loading...', 'mytheme')] ]);
// In ajax.js - access the data console.log(myThemeData.ajaxUrl); fetch(myThemeData.ajaxUrl, { /* ... */ });

Dependencies Management

Ensures scripts/styles load in correct order. WordPress automatically loads dependencies before dependent assets.

┌─────────────────────────────────────────────────────────────┐
│  DEPENDENCY CHAIN                                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   jquery ◄─────┬───── jquery-ui ◄───── my-datepicker       │
│                │                                            │
│                └───── my-slider                             │
│                                                             │
│   Load Order: jquery → jquery-ui → my-datepicker            │
│               jquery → my-slider                            │
└─────────────────────────────────────────────────────────────┘
wp_enqueue_script('my-datepicker', $url, ['jquery-ui-datepicker'], '1.0', true);

Versioning Assets

Appends version query string for cache busting. Use file modification time for automatic cache invalidation during development.

// Static version wp_enqueue_style('main', $url, [], '1.2.3'); // Auto-version based on file modification time (dev-friendly) wp_enqueue_style('main', get_stylesheet_uri(), [], filemtime(get_stylesheet_directory() . '/style.css') ); // Output: style.css?ver=1697385642

Conditional Loading

Load assets only where needed to improve performance. Use WordPress conditional tags to target specific pages.

function mytheme_scripts() { // Always load wp_enqueue_style('main', get_stylesheet_uri()); // Homepage only if (is_front_page()) { wp_enqueue_script('slider', $slider_url); } // Single posts only if (is_single() && comments_open()) { wp_enqueue_script('comment-reply'); } // WooCommerce pages only if (function_exists('is_woocommerce') && is_woocommerce()) { wp_enqueue_style('shop-styles', $shop_url); } } add_action('wp_enqueue_scripts', 'mytheme_scripts');

wp_head()

Essential action hook that outputs all registered styles, scripts, and meta tags in <head>. Must be included before closing </head> tag.

<!-- header.php --> <!DOCTYPE html> <html <?php language_attributes(); ?>> <head> <meta charset="<?php bloginfo('charset'); ?>"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <?php wp_head(); ?> <!-- CRITICAL: Outputs CSS, JS, meta, title --> </head>

wp_footer()

Outputs footer-enqueued scripts and fires wp_footer action. Must be included before closing </body> tag for proper JavaScript loading.

<!-- footer.php --> <footer> <!-- Footer content --> </footer> <?php wp_footer(); ?> <!-- CRITICAL: Outputs footer JS --> </body> </html>

admin_enqueue_scripts

Hook for loading assets specifically in WordPress admin dashboard. Receives $hook_suffix parameter to target specific admin pages.

function mytheme_admin_assets($hook) { // Load on all admin pages wp_enqueue_style('admin-custom', get_template_directory_uri() . '/css/admin.css'); // Only on specific page if ($hook === 'post.php' || $hook === 'post-new.php') { wp_enqueue_script('post-editor-enhancements', $url); } // Only on theme options page if ($hook === 'appearance_page_theme-options') { wp_enqueue_style('theme-options', $url); } } add_action('admin_enqueue_scripts', 'mytheme_admin_assets');

register_nav_menus()

Registers theme menu locations that appear in Appearance → Menus. Allows multiple menus with descriptive labels for site administrators.

function mytheme_register_menus() { register_nav_menus([ 'primary' => __('Primary Menu', 'mytheme'), 'footer' => __('Footer Menu', 'mytheme'), 'mobile' => __('Mobile Navigation', 'mytheme'), 'social' => __('Social Links', 'mytheme'), ]); } add_action('after_setup_theme', 'mytheme_register_menus');

Displays a registered navigation menu with extensive customization options. The primary function for outputting menus in templates.

wp_nav_menu([ 'theme_location' => 'primary', 'container' => 'nav', 'container_class' => 'main-navigation', 'container_id' => 'site-nav', 'menu_class' => 'nav-menu', 'menu_id' => 'primary-menu', 'depth' => 3, 'fallback_cb' => 'mytheme_fallback_menu', ]);

Theme-defined spots where menus can be assigned. Site admins create menus and assign them to locations through the admin interface.

┌─────────────────────────────────────────────────────────────┐
│  MENU LOCATION ARCHITECTURE                                 │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Theme (functions.php)         Admin (Appearance → Menus)   │
│  ─────────────────────         ─────────────────────────    │
│  register_nav_menus([          Menu: "Main Navigation"      │
│    'primary' => 'Header',  ◄── Assigned to: [Primary ✓]     │
│    'footer'  => 'Footer',  ◄── Menu: "Footer Links"         │
│  ]);                           Assigned to: [Footer ✓]      │
│                                                             │
│  Template (header.php)                                      │
│  ─────────────────────                                      │
│  wp_nav_menu([                                              │
│    'theme_location' => 'primary'   ◄── Outputs menu here    │
│  ]);                                                        │
└─────────────────────────────────────────────────────────────┘

Complete control over menu HTML output, including wrapper elements, CSS classes, depth limits, and behavior options.

$args = [ 'theme_location' => 'primary', // Registered location 'menu' => '', // Menu ID, slug, or name 'container' => 'nav', // div, nav, or false 'container_class' => 'menu-wrap', 'container_id' => '', 'menu_class' => 'menu', // UL class 'menu_id' => '', // UL id 'echo' => true, // Echo or return 'fallback_cb' => 'wp_page_menu', // Fallback function 'before' => '', // Before <a> 'after' => '', // After </a> 'link_before' => '', // Before link text 'link_after' => '', // After link text 'items_wrap' => '<ul id="%1$s" class="%2$s">%3$s</ul>', 'depth' => 0, // 0 = all levels 'walker' => '', // Custom walker instance ];

Walker Classes Introduction

Custom Walker classes override default menu HTML output for complete control over markup structure—essential for CSS frameworks like Bootstrap or Tailwind.

class Bootstrap_Nav_Walker extends Walker_Nav_Menu { function start_lvl(&$output, $depth = 0, $args = null) { $output .= '<ul class="dropdown-menu">'; } function start_el(&$output, $item, $depth = 0, $args = null, $id = 0) { $classes = $item->classes ? implode(' ', $item->classes) : ''; $classes .= in_array('menu-item-has-children', $item->classes) ? ' dropdown' : ''; $output .= '<li class="nav-item ' . $classes . '">'; $output .= '<a class="nav-link" href="' . $item->url . '">'; $output .= $item->title . '</a>'; } } // Usage wp_nav_menu(['walker' => new Bootstrap_Nav_Walker()]);

Defines what displays when no menu is assigned to a location. Prevents empty navigation and provides sensible defaults.

// Using built-in page menu fallback wp_nav_menu([ 'theme_location' => 'primary', 'fallback_cb' => 'wp_page_menu', // Shows pages ]); // Custom fallback function function mytheme_fallback_menu() { echo '<nav class="fallback-menu">'; echo '<ul>'; echo '<li><a href="' . home_url() . '">Home</a></li>'; wp_list_pages(['title_li' => '']); echo '</ul></nav>'; } // Disable fallback entirely wp_nav_menu([ 'theme_location' => 'primary', 'fallback_cb' => false, // Shows nothing if no menu ]);

Widget Areas

register_sidebar()

Creates a widget area where users can drag-and-drop widgets via Appearance → Widgets. Define HTML wrapper structure for consistent styling.

function mytheme_widgets_init() { register_sidebar([ 'name' => __('Primary Sidebar', 'mytheme'), 'id' => 'sidebar-1', 'description' => __('Widgets here appear on blog pages.', 'mytheme'), 'before_widget' => '<div id="%1$s" class="widget %2$s">', 'after_widget' => '</div>', 'before_title' => '<h3 class="widget-title">', 'after_title' => '</h3>', ]); } add_action('widgets_init', 'mytheme_widgets_init');

dynamic_sidebar()

Outputs all widgets assigned to a specific widget area. Typically wrapped in conditional check to avoid empty containers.

<!-- sidebar.php --> <aside class="sidebar"> <?php if (is_active_sidebar('sidebar-1')): ?> <?php dynamic_sidebar('sidebar-1'); ?> <?php else: ?> <p>Add widgets to this sidebar.</p> <?php endif; ?> </aside>

is_active_sidebar()

Checks if a widget area has widgets assigned. Prevents rendering empty HTML containers for cleaner markup.

// Conditional sidebar display <?php if (is_active_sidebar('sidebar-1')): ?> <div class="content-with-sidebar"> <main><?php /* content */ ?></main> <aside><?php dynamic_sidebar('sidebar-1'); ?></aside> </div> <?php else: ?> <div class="content-full-width"> <main><?php /* content */ ?></main> </div> <?php endif; ?>

Widget Arguments

HTML wrapper elements applied to each widget consistently. Placeholders %1$s and %2$s inject widget ID and classes automatically.

register_sidebar([ 'name' => 'Footer Widgets', 'id' => 'footer-widgets', 'before_widget' => '<section id="%1$s" class="widget %2$s">', 'after_widget' => '</section>', 'before_title' => '<h4 class="widget-title"><span>', 'after_title' => '</span></h4>', ]);
┌─────────────────────────────────────────────────────────────┐
│  WIDGET HTML OUTPUT STRUCTURE                               │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  <section id="recent-posts-2" class="widget widget_recent"> │
│     ▲                              ▲                        │
│     └── %1$s (auto-generated ID)   └── %2$s (widget class)  │
│                                                             │
│      <h4 class="widget-title"><span>Recent Posts</span></h4>│
│      <ul>                                                   │
│          <li><a href="#">Post Title</a></li>                │
│      </ul>                                                  │
│  </section>                                                 │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Multiple Sidebars

Register multiple widget areas for different page sections (header, footer, sidebar variations). Common pattern for flexible theme layouts.

function mytheme_widgets_init() { $sidebars = [ ['name' => 'Blog Sidebar', 'id' => 'sidebar-blog'], ['name' => 'Page Sidebar', 'id' => 'sidebar-page'], ['name' => 'Footer Col 1', 'id' => 'footer-1'], ['name' => 'Footer Col 2', 'id' => 'footer-2'], ['name' => 'Footer Col 3', 'id' => 'footer-3'], ['name' => 'Header Banner', 'id' => 'header-banner'], ]; foreach ($sidebars as $sidebar) { register_sidebar([ 'name' => __($sidebar['name'], 'mytheme'), 'id' => $sidebar['id'], 'before_widget' => '<div class="widget %2$s">', 'after_widget' => '</div>', 'before_title' => '<h3 class="widget-title">', 'after_title' => '</h3>', ]); } } add_action('widgets_init', 'mytheme_widgets_init');
<!-- footer.php usage --> <footer class="site-footer"> <div class="footer-widgets"> <div class="col"><?php dynamic_sidebar('footer-1'); ?></div> <div class="col"><?php dynamic_sidebar('footer-2'); ?></div> <div class="col"><?php dynamic_sidebar('footer-3'); ?></div> </div> </footer>

Quick Reference Diagram

┌─────────────────────────────────────────────────────────────────────────┐ │ WORDPRESS THEME ARCHITECTURE │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ functions.php │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ after_setup_theme ──► Theme setup, add_theme_support() │ │ │ │ widgets_init ──────► register_sidebar() │ │ │ │ wp_enqueue_scripts ► wp_enqueue_style(), wp_enqueue_script() │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ header.php │ index.php │ footer.php │ │ │ │ ────────── │ ────────── │ ────────── │ │ │ │ wp_head() │ The Loop │ dynamic_sidebar() │ │ │ │ wp_nav_menu() │ the_title() │ wp_footer() │ │ │ │ the_custom_logo() │ the_content() │ │ │ │ │ │ the_excerpt() │ │ │ │ │ │ get_template_part() │ │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘