The Minimum Viable Theme
WordPress requires exactly two files to recognise a directory as a valid theme: style.css and index.php. That's it. Everything else — header.php, footer.php, single.php — is optional and loaded by the template hierarchy only when it exists.
Create a folder inside wp-content/themes/, for example my-theme/, and add these two files:
/*
* Theme Name: My Theme
* Theme URI: https://lucasnmoura.com
* Author: Lucas Moura
* Description: Custom theme built from scratch.
* Version: 1.0.0
* License: GPL-2.0-or-later
* Text Domain: my-theme
*/
That comment block in style.css is the theme manifest — WordPress reads it to populate the Appearance > Themes screen. Activate the theme and you have a working (blank) site.
Recommended File Structure
A clean, scalable theme follows a predictable layout that separates concerns and maps directly to WordPress's template hierarchy:
my-theme/
├── style.css # Theme manifest + global styles
├── functions.php # Hooks, enqueue, theme support
├── index.php # Fallback template (required)
├── header.php # get_header() target
├── footer.php # get_footer() target
├── single.php # Single post view
├── page.php # Static page view
├── archive.php # Post archive
├── search.php # Search results
├── 404.php # Not found
├── template-parts/
│ ├── content.php
│ ├── content-single.php
│ └── content-none.php
└── assets/
├── css/
│ └── main.css
└── js/
└── main.js
Understanding the Template Hierarchy
When a visitor hits a URL, WordPress walks a decision tree to find the right template file. For a single post, the lookup order is:
single-{post-type}-{slug}.phpsingle-{post-type}.phpsingle.phpsingular.phpindex.php
WordPress uses the first file it finds. This means you can create a single-product.php to handle WooCommerce products differently from regular posts without touching a plugin file.
functions.php — The Theme's Engine
Think of functions.php as a theme-scoped plugin. It runs on every request and is the right place to declare theme support, register menus, enqueue assets, and add custom hooks.
<?php
// Theme setup
add_action( 'after_setup_theme', function() {
add_theme_support( 'title-tag' );
add_theme_support( 'post-thumbnails' );
add_theme_support( 'html5', [
'search-form', 'comment-form', 'gallery', 'caption',
] );
register_nav_menus( [
'primary' => __( 'Primary Menu', 'my-theme' ),
'footer' => __( 'Footer Menu', 'my-theme' ),
] );
} );
// Enqueue assets
add_action( 'wp_enqueue_scripts', function() {
wp_enqueue_style(
'my-theme-style',
get_template_directory_uri() . '/assets/css/main.css',
[],
wp_get_theme()->get( 'Version' )
);
wp_enqueue_script(
'my-theme-script',
get_template_directory_uri() . '/assets/js/main.js',
[],
wp_get_theme()->get( 'Version' ),
true // load in footer
);
} );
Always version your assets using the theme version number. This busts the cache automatically whenever you update the theme.
The Loop
The Loop is the core pattern that powers every WordPress template. It iterates over the query results and outputs whatever HTML you place inside it:
<?php if ( have_posts() ) : ?>
<?php while ( have_posts() ) : the_post(); ?>
<article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
<header>
<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>
</header>
<div class="entry-content">
<?php the_content(); ?>
</div>
</article>
<?php endwhile; ?>
<?php else : ?>
<p>No posts found.</p>
<?php endif; ?>
Header and Footer
Split your layout with get_header() and get_footer(). These functions include header.php and footer.php respectively. A minimal 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(); ?> <!-- NEVER remove this -->
</head>
<body <?php body_class(); ?>>
<?php wp_body_open(); ?>
<header class="site-header">
<a href="<?php echo home_url('/'); ?>">
<?php bloginfo( 'name' ); ?>
</a>
<nav>
<?php wp_nav_menu( [ 'theme_location' => 'primary' ] ); ?>
</nav>
</header>
wp_head() and wp_footer(). Plugins and WordPress core rely on these hooks to inject scripts, styles, and meta tags. Removing them breaks half your plugins silently.
Template Parts
Use get_template_part() to extract reusable chunks out of your templates. This keeps individual template files short and composable:
<?php
// In index.php — delegate per-post HTML to a partial
while ( have_posts() ) :
the_post();
get_template_part( 'template-parts/content', get_post_type() );
endwhile;
WordPress will look for template-parts/content-post.php first, then fall back to template-parts/content.php. This mirrors the template hierarchy pattern and lets you specialise output per post type without branching logic in the parent template.
Shipping a Production Theme
Before going live, run through this checklist:
- All strings wrapped in
__( 'text', 'my-theme' )for i18n - All output escaped:
esc_html(),esc_url(),esc_attr() - Assets versioned and conditionally loaded (only on pages that need them)
wp_head()andwp_footer()present in every layout- Theme tested with Theme Check plugin
- Tested with
WP_DEBUGandWP_DEBUG_LOGenabled
A hand-built theme is never "done" — but starting from zero teaches you exactly what page builders abstract away, and that knowledge compounds across every project you'll ever build.