🗂️ Theme File Structure and Template Hierarchy
A WordPress theme is just a folder of files — but which file renders the page you're looking at? That answer is decided by the template hierarchy, one of WordPress's most elegant ideas. Once it clicks, you can control exactly how any post, page, category, or archive looks.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Identify the required and common files in a classic WordPress theme and describe each file's job
- Explain how the template hierarchy picks a template file, from most specific to the
index.phpfallback - Trace which template WordPress loads for a single post, a page, and a category archive
- Use
get_template_part()to split templates into reusable components - Build a child theme to customize a parent without losing changes on update
Estimated Time: 35–45 minutes • Difficulty: Intermediate
Hands-on: Predict the template WordPress loads for several URLs, then build a minimal working theme skeleton.
In This Lesson
Themes: Presentation, Not Function
WordPress splits cleanly into two halves. WordPress core handles functionality and data: it stores posts in the database, runs the admin dashboard, and exposes an API of functions. A theme handles presentation: it decides how that content looks and where each piece appears on screen.
💡 A useful analogy: Think of WordPress core as the plumbing, wiring, and load-bearing walls of a house. The theme is the paint, the furniture, and the layout of each room. You can completely redecorate — swap themes — without touching the foundation or losing a single post.
This separation is the single most important idea in theme development. It tells you what belongs in a theme and, just as importantly, what does not.
✅ What a theme should control
- Visual design — colors, typography, spacing
- Layout — columns, grids, where the sidebar sits
- Navigation menus, widget areas, and responsive behavior
- How each content type (post, page, product) is presented
⚠️ What a theme should NOT control
- Custom post types or taxonomies the site depends on
- Business logic and data processing
- Anything that must survive a theme switch
Functionality that would be lost when the user changes themes belongs in a plugin, not the theme. This is often called the "theme vs. plugin" rule.
Core supplies the data and the APIs; the theme decides how it all looks.
The Theme Directory
Every theme lives in its own folder inside /wp-content/themes/. Installing a theme means dropping a folder there; activating it happens under Appearance → Themes. Here is the anatomy of a typical classic (PHP) theme:
wp-content/
└── themes/
├── twentytwentyfour/ # A bundled default theme
└── my-theme/ # Your custom theme
├── style.css # REQUIRED — metadata header + styles
├── index.php # REQUIRED — universal fallback template
├── functions.php # Theme setup, hooks, enqueues (mini-plugin)
├── header.php # Opening HTML + site header
├── footer.php # Site footer + closing HTML
├── sidebar.php # Widget area
├── front-page.php # The site's front page
├── home.php # The blog posts index
├── single.php # A single blog post
├── page.php # A static page
├── archive.php # Category/tag/date/author archives
├── search.php # Search results
├── 404.php # "Not found" page
├── comments.php # Comment list + form
├── template-parts/ # Reusable fragments
│ ├── content.php
│ └── content-page.php
├── inc/ # PHP includes (customizer, template tags)
├── assets/ # css/, js/, images/
├── screenshot.png # 1200×900 thumbnail for the admin
└── theme.json # Global styles & settings (modern themes)
Only two of these files are strictly required. Everything else is an optional refinement that gives you more precise control — and cleaner code.
The Two Required Files
A theme that WordPress will recognize and activate needs exactly two files: style.css and index.php.
style.css — the identity card
Despite the name, this file does double duty. Its opening comment block is metadata that WordPress reads to display your theme in the admin. Below that comment you may add CSS, though larger themes usually enqueue separate stylesheets instead.
/*
Theme Name: My Theme
Theme URI: https://example.com/my-theme
Author: Ray de la Paz
Author URI: https://example.com
Description: A lean, modern classic theme.
Version: 1.0.0
Requires at least: 6.4
Tested up to: 6.7
Requires PHP: 8.2
License: GNU General Public License v2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
Text Domain: my-theme
Tags: blog, custom-menu, featured-images, translation-ready
*/
/* Optional theme styles below the header */
body {
font-family: system-ui, sans-serif;
line-height: 1.6;
}
📖 Why the header matters
Delete or corrupt this comment block and WordPress simply won't list the theme. Text Domain connects your theme to its translation files, and Requires PHP stops the theme from activating on an incompatible server. Treat it like a product label.
index.php — the universal fallback
If WordPress can't find a more specific template, it always falls back to index.php. That guarantee is why it's required. A minimal but complete index.php looks like this:
<?php get_header(); ?>
<main id="primary" class="site-main">
<?php
if ( have_posts() ) :
while ( have_posts() ) :
the_post();
?>
<article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
<h2 class="entry-title">
<a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
</h2>
<div class="entry-content">
<?php the_content(); ?>
</div>
</article>
<?php
endwhile;
the_posts_navigation();
else :
?>
<p><?php esc_html_e( 'Nothing found.', 'my-theme' ); ?></p>
<?php
endif;
?>
</main>
<?php get_sidebar(); ?>
<?php get_footer(); ?>
Notice this file no longer hand-writes the <html> and <head> tags — those move into header.php, pulled in by get_header(). That is the modular approach we'll explore below.
💡 functions.php: the theme's brain
Though not required, almost every real theme includes functions.php. It runs on every request and is where you register menus, widget areas, image sizes, and enqueue your CSS/JS. Think of it as a mini-plugin bundled with the theme.
<?php
// functions.php — theme setup
function mytheme_setup() {
add_theme_support( 'title-tag' );
add_theme_support( 'post-thumbnails' );
add_theme_support( 'html5', array( 'search-form', 'comment-form', 'gallery', 'caption' ) );
add_theme_support( 'responsive-embeds' );
register_nav_menus( array(
'primary' => __( 'Primary Menu', 'my-theme' ),
'footer' => __( 'Footer Menu', 'my-theme' ),
) );
}
add_action( 'after_setup_theme', 'mytheme_setup' );
function mytheme_assets() {
wp_enqueue_style( 'my-theme', get_stylesheet_uri(), array(), wp_get_theme()->get( 'Version' ) );
wp_enqueue_script( 'my-theme-nav', get_theme_file_uri( 'assets/js/navigation.js' ), array(), '1.0.0', true );
}
add_action( 'wp_enqueue_scripts', 'mytheme_assets' );
The Template Hierarchy
Here is the core idea, and it's worth memorizing: for any request, WordPress looks for the most specific template file it can find, and falls back to more general ones until it lands on index.php.
💡 Think of hospital triage. WordPress first asks "what kind of request is this?" (a single post? a category? a 404?). Then, within that category, it looks for the most specialized template available before settling for a general one. You never write a template for a case that doesn't need special treatment — the fallbacks handle it.
The hierarchy branches by content type. This simplified flowchart shows the major branches and their fallbacks:
Every path eventually funnels down to index.php.
📖 Where the full map lives
The complete, official diagram covers dozens of cases (attachments, custom taxonomies, embeds, and more). Bookmark it: developer.wordpress.org — Template Hierarchy. You don't need to memorize it — you need to understand the pattern of specific → general.
Tracing Real Requests
The best way to internalize the hierarchy is to trace concrete URLs. WordPress checks each template in order and stops at the first that exists.
| Request | Templates checked, in order |
|---|---|
A blog post at /2026/05/my-post/ |
single-post.php → single.php → singular.php → index.php |
A product (custom type) at /product/chair/ |
single-product-chair.php → single-product.php → single.php → singular.php → index.php |
The About page at /about/ |
page-about.php → page-{id}.php → page.php → singular.php → index.php |
Category archive at /category/news/ |
category-news.php → category-{id}.php → category.php → archive.php → index.php |
| The site front page | front-page.php → then home.php (blog) or the page templates (static) |
💡 Practical payoff
Want a distinctive look for just your "Sale" category? Create category-sale.php — every other category keeps using category.php. Need a unique layout for case studies? Add single-case-study.php. You customize exactly what needs it and let the fallbacks handle the rest.
⚠️ front-page vs. home — a classic gotcha
home.php is the blog posts index, not necessarily your homepage. front-page.php always wins for whatever is set as the site's front page under Settings → Reading. If a static page is your front page but front-page.php exists, it takes precedence over the page templates — a frequent source of "why isn't my page template loading?" confusion.
Template Parts & Reuse
Copy-pasting the same post markup into index.php, single.php, and archive.php is a maintenance trap. WordPress solves this with get_template_part(), which pulls in a reusable fragment and — crucially — respects child themes and naming fallbacks.
<?php
// Load template-parts/content.php
get_template_part( 'template-parts/content' );
// Load template-parts/content-video.php, falling back to content.php
get_template_part( 'template-parts/content', 'video' );
// Pass data to the part (WordPress 5.5+)
get_template_part( 'template-parts/content', 'post', array(
'featured' => true,
) );
A slimmed single.php then reads almost like an outline of the page:
<?php get_header(); ?>
<main id="primary" class="site-main">
<?php
while ( have_posts() ) :
the_post();
// content-post.php if it exists, else content.php
get_template_part( 'template-parts/content', get_post_type() );
if ( comments_open() || get_comments_number() ) {
comments_template();
}
endwhile;
?>
</main>
<?php get_sidebar(); ?>
<?php get_footer(); ?>
Inside the part, you read passed data from the $args array:
<?php
// template-parts/content.php
$args = wp_parse_args( $args ?? array(), array( 'featured' => false ) );
?>
<article id="post-<?php the_ID(); ?>" <?php post_class( $args['featured'] ? 'is-featured' : '' ); ?>>
<?php the_title( '<h2 class="entry-title">', '</h2>' ); ?>
<div class="entry-content"><?php the_excerpt(); ?></div>
</article>
✅ Why this pays off
- DRY: fix the post markup in one place, everywhere updates.
- Overridable: a child theme can replace a single part without touching the parent.
- Readable: your main templates become high-level outlines of the page.
Child Themes
Suppose you love a theme but want to tweak its header and colors. Edit the theme directly and the next update overwrites your work. The fix is a child theme: a separate theme that inherits everything from a parent and overrides only what you change.
💡 It's inheritance. A child theme is to its parent what a subclass is to a base class: it keeps all the parent's behavior for free and overrides individual methods (here, template files) as needed.
A child theme needs a folder and a style.css with a Template line pointing at the parent's folder name:
/*
Theme Name: Twenty Twenty-Four Child
Template: twentytwentyfour
Version: 1.0.0
Text Domain: twentytwentyfour-child
*/
The Template value must match the parent folder exactly. Then load both stylesheets from the child's functions.php:
<?php
function child_enqueue_styles() {
// Parent stylesheet
wp_enqueue_style(
'parent-style',
get_template_directory_uri() . '/style.css',
array(),
wp_get_theme( get_template() )->get( 'Version' )
);
// Child stylesheet, dependent on the parent
wp_enqueue_style(
'child-style',
get_stylesheet_uri(),
array( 'parent-style' ),
wp_get_theme()->get( 'Version' )
);
}
add_action( 'wp_enqueue_scripts', 'child_enqueue_styles' );
📖 get_template vs. get_stylesheet
In a child theme these diverge: get_template_directory() points to the parent, while get_stylesheet_directory() points to the child. Use the stylesheet functions for your own child files and the template functions when referencing the parent.
To override any template — say the header — copy the parent's header.php into the child folder and edit it. WordPress uses the child's version automatically. The same works for template parts: a child's template-parts/content.php transparently replaces the parent's.
⚠️ When a child theme isn't the answer
Child themes shine when you customize a well-built, actively updated parent. If you're rewriting nearly everything, or the parent is abandoned, you're better off building a standalone theme.
Hands-on Exercise
🏋️ Part A — Predict the template
Objective: Confirm you can walk the hierarchy in your head. For each request, write the ordered list of templates WordPress checks. Assume a theme that contains only index.php, single.php, page.php, and archive.php.
- A single blog post
- A tag archive at
/tag/php/ - A static page at
/contact/
💡 Hint
Write the full ideal chain first (most specific → index.php), then cross out any file that doesn't exist in this theme. Whatever remains at the top is the winner.
✅ Solution
- Ideal:
single-post.php→single.php→singular.php→index.php. Winner:single.php. - Ideal:
tag-php.php→tag.php→archive.php→index.php. Winner:archive.php. - Ideal:
page-contact.php→page-{id}.php→page.php→singular.php→index.php. Winner:page.php.
🏋️ Part B — Build a minimal theme
Objective: Create a theme that WordPress will activate, with a header/footer split.
- Make a folder
my-first-themeinwp-content/themes/. - Add
style.csswith a valid metadata header. - Add
header.php,footer.php, and anindex.phpthat callsget_header(), runs the Loop, and callsget_footer(). - Activate it under Appearance → Themes and confirm your posts render.
✅ Sample header.php
<!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 class="site-header">
<a href="<?php echo esc_url( home_url( '/' ) ); ?>"><?php bloginfo( 'name' ); ?></a>
</header>
The matching footer.php must call wp_footer() before </body> so plugins and core scripts load correctly.
🎯 Quick Quiz
Question 1: Which two files are the absolute minimum for a valid classic WordPress theme?
Question 2: A visitor views a single blog post. Your theme has index.php and archive.php but no single.php. Which template renders the post?
Question 3: Why build a child theme instead of editing the parent directly?
Best Practices
✅ Do
- Keep functionality that must survive theme switches in a plugin.
- Enqueue scripts and styles with
wp_enqueue_*— never hard-code<link>/<script>tags. - Escape output (
esc_html,esc_url,esc_attr) and use a text domain for translatable strings. - Split templates with
get_template_part()to stay DRY. - Reference files with
get_theme_file_uri()/get_stylesheet_directory()instead of hard paths.
⚠️ Don't
- Don't edit a parent theme you didn't author — use a child theme.
- Don't forget
wp_head()in the head andwp_footer()before</body>— many plugins break without them. - Don't register custom post types inside a theme if the content must persist across theme changes.
Summary & Quiz
🎉 Key Takeaways
- Themes handle presentation; core and plugins handle function and data.
- A valid theme needs only
style.css(metadata header) andindex.php(fallback). - The template hierarchy chooses the most specific template available and falls back toward
index.php. get_template_part()keeps templates DRY and override-friendly.- Child themes let you customize a parent without losing changes on update.
📚 Further Reading
🚀 What's Next?
Now that you know which template file runs, the next lesson dives into what goes inside it: The WordPress Loop — the engine that pulls your posts out of the database and displays them.
🎉 Great work!
You can now read a theme folder like a map and predict exactly which file renders any page.