Luke Anderson WordPress

WordPress Theme Development from Scratch — Complete Step-by-Step Guide

Most “WordPress tutorials” stop at installing a pre-made theme and tweaking colors. This one doesn’t. In this guide, you’ll build a complete, custom WordPress theme from absolute zero — starting with an HTML template and ending with a fully functional WordPress site that has a dynamic slider, blog with pagination, single post pages, comments, categories, tags, widgets, and even a custom media upload plugin.

By the end you’ll know exactly how WordPress themes work under the hood, and you’ll be able to convert any HTML template into a WordPress theme in a few hours.

📺 Watch the full video walkthrough (2h 25m): https://youtu.be/6RS92BJeREI

The video shows every step in real time, including a Redis performance test, Tailwind CSS setup, and BrowserSync live-reload configuration. Bookmark this post — it’s the written companion you can come back to.

What You’ll Build

By the end of this guide your custom theme will have:

  • ✅ A WordPress-compliant theme structure (recognized in Appearance → Themes)
  • ✅ Header and footer converted from a real HTML template into reusable PHP includes
  • ✅ A homepage with a dynamic slider powered by a custom post type
  • ✅ Tailwind CSS integrated with live-reload via BrowserSync
  • ✅ A dynamic navigation menu manageable from WordPress admin
  • ✅ A blog page with proper pagination
  • ✅ A single post page with featured image, content, and comments
  • ✅ A custom comment form
  • ✅ Category, tag, and recent-posts widgets
  • ✅ A Must-Use Plugin for the slider custom post type
  • ✅ A Custom Media Plugin demonstrating database operations

This is professional theme development — the same approach used by ThemeForest authors and agency teams.

Prerequisites

Before starting, you should have:

  • Local WordPress install running (we used Docker — see our WSL + Docker + PHP setup guide)
  • A code editor (PhpStorm, VS Code, Sublime — anything you’re comfortable with)
  • Basic HTML, CSS, and PHP knowledge (you don’t need to be an expert; following along is enough)
  • A free HTML template to convert (the video uses Mediplus Lite, a free medical template, but any template works)

💡 No WordPress install yet? The fastest way is Docker. Run docker-compose up -d with a wordpress + mysql setup and you’ll have WordPress on http://localhost:81 in 60 seconds. We cover the full Docker setup in our linked guide above.

Understanding How WordPress Themes Work

Before writing code, understand this: a WordPress theme is just a folder inside wp-content/themes/ containing PHP, CSS, and JS files. WordPress looks for specific filenames and uses them to render pages.

The minimum required files are:

File Purpose
style.css Theme metadata + main stylesheet
index.php Fallback template for every page

Everything else is optional — but most themes add header.php, footer.php, functions.php, front-page.php, single.php, page.php, comments.php, etc. WordPress falls back to index.php for any template that doesn’t exist (this is called the template hierarchy).

Step 1: Create Your Theme Folder

Navigate to your WordPress install and create a new folder under wp-content/themes/:

wp-content/
└── themes/
    └── my-custom-theme/      ← your theme folder
        ├── style.css
        ├── index.php
        └── screenshot.png   (optional — 1200×900 preview image)

Use lowercase letters and hyphens for the folder name. WordPress uses this as the theme’s internal name.

Create the bare-minimum style.css

WordPress identifies themes by a special header comment at the top of style.css. Without this, WordPress won’t recognize your theme:

/*
Theme Name: My Custom Theme
Theme URI: https://cloudywp.com.au/
Author: Your Name
Author URI: https://cloudywp.com.au/
Description: A custom WordPress theme built from scratch.
Version: 1.0.0
License: GNU General Public License v2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
Text Domain: my-custom-theme
*/

Create the bare-minimum index.php

<?php
// index.php — fallback template for everything
?>
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
    <meta charset="<?php bloginfo('charset'); ?>">
    <?php wp_head(); ?>
</head>
<body <?php body_class(); ?>>
    <h1><?php bloginfo('name'); ?></h1>
    <p><?php bloginfo('description'); ?></p>

    <?php
    if (have_posts()) :
        while (have_posts()) : the_post();
            the_title('<h2>', '</h2>');
            the_content();
        endwhile;
    endif;
    wp_footer();
    ?>
</body>
</html>

Activate your theme at WordPress Admin → Appearance → Themes → My Custom Theme → Activate.

🎉 You now have a working WordPress theme. It’s ugly, but it’s real.

Step 2: Configure WordPress Reading Settings

Before building further, set WordPress to use static pages for homepage and blog:

  1. Go to Settings → Reading
  2. Set “Your homepage displays” to “A static page”
  3. Create two pages first (Pages → Add New): one called Home, another called Blog
  4. Set Homepage to “Home” and Posts page to “Blog”
  5. Check “Discourage search engines” during development so Google doesn’t index your localhost
  6. Save Changes

This tells WordPress: “Use my front-page.php for the homepage, and index.php for the blog listing.” Critical for the next steps.

Step 3: Get an HTML Template

You can build the design from scratch or use any free HTML template. For tutorials, free templates are perfect because they’re complete and tested. The video uses Mediplus Lite — a free medical/doctor HTML template — but any modern HTML template works.

Your downloaded template folder will look something like:

mediplus-lite/
├── css/
├── fonts/
├── img/
├── js/
├── mail/
├── index.html
├── blog-single.html
├── contact.html
├── portfolio-details.html
└── style.css

Copy the entire css/, js/, fonts/, img/ folders into your theme folder. You’ll reference them later via get_template_directory_uri().

Step 4: Split HTML Into header.php and footer.php

Open index.html from the template. You’ll see something like:

<!DOCTYPE html>
<html>
<head>
    <title>...</title>
    <link rel="stylesheet" href="css/bootstrap.min.css">
    <!-- more CSS -->
</head>
<body>
    <header>...</header>

    <!-- MAIN CONTENT -->

    <footer>...</footer>

    <script src="js/jquery.min.js"></script>
    <!-- more JS -->
</body>
</html>

Split it into three pieces:

header.php — top of every page

<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
    <meta charset="<?php bloginfo('charset'); ?>">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><?php wp_title(); ?></title>

    <!-- Bootstrap -->
    <link rel="stylesheet" href="<?php echo get_template_directory_uri(); ?>/css/bootstrap.min.css">
    <!-- Nice Select -->
    <link rel="stylesheet" href="<?php echo get_template_directory_uri(); ?>/css/nice-select.css">
    <!-- Font Awesome -->
    <link rel="stylesheet" href="<?php echo get_template_directory_uri(); ?>/css/font-awesome.min.css">
    <!-- Icofont -->
    <link rel="stylesheet" href="<?php echo get_template_directory_uri(); ?>/css/icofont.css">
    <!-- Slicknav -->
    <link rel="stylesheet" href="<?php echo get_template_directory_uri(); ?>/css/slicknav.min.css">
    <!-- Owl Carousel -->
    <link rel="stylesheet" href="<?php echo get_template_directory_uri(); ?>/css/owl-carousel.css">
    <!-- Datepicker -->
    <link rel="stylesheet" href="<?php echo get_template_directory_uri(); ?>/css/datepicker.css">
    <!-- Animate -->
    <link rel="stylesheet" href="<?php echo get_template_directory_uri(); ?>/css/animate.min.css">
    <!-- Main Style -->
    <link rel="stylesheet" href="<?php echo get_stylesheet_uri(); ?>">

    <?php wp_head(); ?>
</head>
<body <?php body_class(); ?>>

    <header>
        <!-- Paste your template's header HTML here -->
    </header>

🔑 Key concept: get_template_directory_uri() returns the URL to your theme folder. NEVER hard-code paths like /wp-content/themes/my-theme/css/style.css — it will break if the theme is renamed or moved.

footer.php — bottom of every page

    <footer>
        <!-- Paste your template's footer HTML here -->
    </footer>

    <!-- JavaScript -->
    <script src="<?php echo get_template_directory_uri(); ?>/js/jquery.min.js"></script>
    <script src="<?php echo get_template_directory_uri(); ?>/js/bootstrap.min.js"></script>
    <script src="<?php echo get_template_directory_uri(); ?>/js/main.js"></script>

    <?php wp_footer(); ?>
</body>
</html>

Update index.php to use them

<?php get_header(); ?>

<main>
    <?php
    if (have_posts()) :
        while (have_posts()) : the_post();
            the_title('<h2>', '</h2>');
            the_content();
        endwhile;
    endif;
    ?>
</main>

<?php get_footer(); ?>

Now every page automatically includes the header and footer. Edit header.php once → updates everywhere.

Step 5: Create the Front Page (Homepage)

WordPress looks for front-page.php first when displaying the homepage. Create it:

<?php get_header(); ?>

<!-- Slider Area -->
<section class="slider">
    <div class="hero-slider">
        <?php
        $args = [
            'post_type' => 'slider',
            'posts_per_page' => -1
        ];
        $slider_query = new WP_Query($args);

        if ($slider_query->have_posts()) :
            while ($slider_query->have_posts()) : $slider_query->the_post();

                // Get featured image URL
                $thumbnail_id = get_post_thumbnail_id();
                $thumbnail_url = '';
                if ($thumbnail_id) {
                    $thumbnail_url = wp_get_attachment_image_src($thumbnail_id, 'full', true);
                }
                ?>

                <!-- Start Single Slider -->
                <div class="single-slider" style="background-image:url('<?php echo esc_url($thumbnail_url[0]); ?>')">
                    <div class="container">
                        <div class="row">
                            <div class="col-lg-7">
                                <div class="text">
                                    <h1><?php the_content(); ?><span>Trust!</span></h1>
                                    <div class="button">
                                        <a href="#" class="btn">
                                            <?php
                                            $custom_field_value = get_post_meta(get_the_ID(), 'button_1', true);
                                            echo esc_html($custom_field_value);
                                            ?>
                                        </a>
                                        <a href="#" class="btn primary">Learn More</a>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
                <!-- End Single Slider -->

            <?php endwhile;
            wp_reset_postdata();
        endif;
        ?>
    </div>
</section>

<!-- Other homepage sections (paste from your template) -->

<?php get_footer(); ?>

But wait — 'post_type' => 'slider' references a custom post type that doesn’t exist yet. We’ll create it in the next step using a Must-Use Plugin.

Step 6: Create a Must-Use Plugin for the Slider

A Must-Use Plugin (MU plugin) is a plugin that’s always active and can’t be deactivated from the admin. They live in wp-content/mu-plugins/. Perfect for custom post types that the theme depends on.

Create wp-content/mu-plugins/custom-slider.php:

<?php
/*
Plugin Name: Custom Slider
Description: Registers the Slider custom post type for the homepage hero.
Version: 1.0.0
Author: You
*/

function register_slider_post_type() {
    $args = [
        'labels' => [
            'name' => 'Sliders',
            'singular_name' => 'Slider',
            'add_new' => 'Add New Slide',
            'add_new_item' => 'Add New Slide',
            'edit_item' => 'Edit Slide',
        ],
        'public' => true,
        'has_archive' => false,
        'supports' => ['title', 'editor', 'thumbnail', 'custom-fields'],
        'menu_icon' => 'dashicons-format-gallery',
        'show_in_rest' => true,
    ];
    register_post_type('slider', $args);
}
add_action('init', 'register_slider_post_type');

Now in WordPress Admin you’ll see a new “Sliders” menu item. Add a few slides, upload featured images, add a custom field called button_1 with text like “Book Appointment”, and your homepage slider will render dynamically.

💡 Why MU plugin instead of inside functions.php? Custom post types are data, not design. If a user switches themes, their slider data shouldn’t disappear. MU plugins persist across theme changes.

Step 7: Set Up Tailwind CSS with Live Reload

For modern theme development, Tailwind + BrowserSync is the productivity sweet spot. Inside your theme folder, create package.json:

{
  "name": "my-custom-theme",
  "version": "1.0.0",
  "description": "Custom WordPress theme",
  "main": "tailwind.config.js",
  "scripts": {
    "build": "tailwindcss build tailwind.css -o style.css",
    "start": "browser-sync start --proxy http://localhost:81 --files 'style.css, *.css, *.php, ./*.php, *.php, .//*.php'"
  },
  "author": "You",
  "license": "ISC",
  "devDependencies": {
    "tailwindcss": "^3.4.0",
    "browser-sync": "^3.0.0"
  }
}

Then run:

cd wp-content/themes/my-custom-theme
npm install
npx tailwindcss init

Create tailwind.css in your theme folder:

@tailwind base;
@tailwind components;
@tailwind utilities;

Run two terminals side by side:

# Terminal 1: rebuild Tailwind on every save
npm run build -- --watch

# Terminal 2: live-reload the browser
npm run start

Now every save in your .php files reloads the browser automatically. No more F5 spamming.

Step 8: Properly Enqueue Styles via functions.php

Hard-coding <link> tags in header.php works, but the WordPress way is to enqueue them through functions.php. This lets plugins and child themes interact with your styles, and adds version-cache-busting automatically.

Create functions.php:

<?php

// Theme Setup
function my_theme_setup() {
    // Add featured images support
    add_theme_support('post-thumbnails');

    // Add title tag support
    add_theme_support('title-tag');

    // Register nav menus
    register_nav_menus([
        'primary' => ('Primary Menu', 'my-custom-theme'),
        'footer'  => ('Footer Menu', 'my-custom-theme'),
    ]);
}
add_action('after_setup_theme', 'my_theme_setup');


// Enqueue styles and scripts
function my_theme_enqueue_assets() {
    $theme_uri = get_template_directory_uri();
    $version = wp_get_theme()->get('Version');

    // Styles
    wp_enqueue_style('bootstrap', $theme_uri . '/css/bootstrap.min.css', [], $version);
    wp_enqueue_style('font-awesome', $theme_uri . '/css/font-awesome.min.css', [], $version);
    wp_enqueue_style('owl-carousel', $theme_uri . '/css/owl-carousel.css', [], $version);
    wp_enqueue_style('animate', $theme_uri . '/css/animate.min.css', [], $version);
    wp_enqueue_style('main-style', get_stylesheet_uri(), [], $version);

    // Scripts (loaded in footer)
    wp_enqueue_script('jquery'); // WordPress bundles jQuery
    wp_enqueue_script('bootstrap-js', $theme_uri . '/js/bootstrap.min.js', ['jquery'], $version, true);
    wp_enqueue_script('owl-carousel-js', $theme_uri . '/js/owl.carousel.min.js', ['jquery'], $version, true);
    wp_enqueue_script('main-js', $theme_uri . '/js/main.js', ['jquery'], $version, true);
}
add_action('wp_enqueue_scripts', 'my_theme_enqueue_assets');


// Widget areas
function theme_widgets_init() {
    $args = [
        'name' => ('Sidebar'),
        'id' => 'sidebar_widget',
        'description' => 'Registering widget area',
        'before_widget' => '<section id="someId">',
        'after_widget' => '</section>',
        'before_title' => '<h2 class="widget-title">',
        'after_title' => '</h2>',
    ];
    register_sidebar($args);
}
add_action('widgets_init', 'theme_widgets_init');

Now remove all the hard-coded <link> tags from header.php. WordPress will inject them automatically through wp_head().

Step 9: Dynamic Navigation Menu

In your header.php, replace the static <ul><li> navigation with WordPress’s dynamic menu:

<nav>
    <?php
    wp_nav_menu([
        'theme_location' => 'primary',
        'menu_class'     => 'main-menu',
        'container'      => false,
    ]);
    ?>
</nav>

Then in WordPress Admin → Appearance → Menus, create a menu, add pages/links, and assign it to the “Primary Menu” location. The site updates immediately.

Step 10: Blog Page with Pagination

Your index.php becomes the blog page (because Settings → Reading sets it as the “Posts page”). Add proper pagination:

<?php get_header(); ?>

<div class="blog-listing container">
    <div class="row">
        <?php
        if (have_posts()) :
            $index = 0;
            while (have_posts()) : the_post();
                ?>
                <div class="col-lg-4 col-md-6">
                    <a href="<?php the_permalink(); ?>">
                        <?php if (has_post_thumbnail()) : ?>
                            <?php the_post_thumbnail('medium'); ?>
                        <?php endif; ?>

                        <h3><?php the_title(); ?></h3>

                        <p class="text-gray-500 mt-5">
                            <?php echo get_the_excerpt(); ?>
                        </p>
                    </a>
                </div>
                <?php
                $index++;
            endwhile;
        endif;
        ?>
    </div>

    <div class="pagination">
        <?php
        the_posts_pagination([
            'prev_text' => ('Previous page'),
            'next_text' => ('Next Page'),
            'before_page_number' => '',
        ]);
        ?>
    </div>
</div>

<?php get_footer(); ?>

the_posts_pagination() handles all the heavy lifting — generating numbered page links, prev/next arrows, current-page styling, all automatically.

Step 11: Single Post Page

Create single.php for individual blog posts:

<?php get_header(); ?>

<article class="single-post container">
    <?php
    if (have_posts()) :
        while (have_posts()) : the_post();
            ?>

            <h1><?php the_title(); ?></h1>

            <div class="meta">
                <span>By <?php the_author(); ?></span>
                <span>on <?php echo get_the_date(); ?></span>
                <span>in <?php the_category(', '); ?></span>
            </div>

            <?php if (has_post_thumbnail()) : ?>
                <?php the_post_thumbnail('large'); ?>
            <?php endif; ?>

            <div class="content">
                <?php the_content(); ?>
            </div>

            <div class="tags"><?php the_tags('Tags: ', ', '); ?></div>

            <div class="row">
                <div class="col-12">
                    <?php
                    if (comments_open() || get_comments_number()) :
                        comments_template();
                    endif;
                    ?>
                </div>

                <div class="col-12">
                    <?php if (get_comments_number()) : ?>
                        <div class="blog-comments">
                            <h2>All Comments</h2>
                            <div class="comments-body">
                                <!-- Single Comments -->
                                <div class="single-comments"><!-- comment markup --></div>
                            </div>
                        </div>
                    <?php endif; ?>
                </div>
            </div>

            <?php
        endwhile;
    endif;
    ?>
</article>

<?php get_footer(); ?>

Step 12: Comments System with Custom Form

Create comments.php for the comment area:

<div id="comments" class="comments-area">
    <?php
    if (!comments_open() && get_comments_number() && get_type_supports(get_post_type(), 'comments')) :
        ?>
        <p class="no-comments">
            <?php esc_html_e('Comments are Closed'); ?>
        </p>
        <?php
    endif;

    $custom_comment_form_path = get_template_directory() . '/custom-comment-form.php';
    if (file_exists($custom_comment_form_path)) {
        require $custom_comment_form_path;
    } else {
        comment_form();
    }
    ?>
</div>

Custom comment form

Create custom-comment-form.php to override the default WordPress form with your own design:

<?php
$commenter = wp_get_current_commenter();
$req = get_option('require_name_email');
$aria_req = ($req ? " aria-required='true'" : '');

$args = [
    'title_reply' => ('Leave a Reply'),
    'label_submit' => ('Post Comment'),
    'class_form' => 'comment-form custom-comment-form',
    'comment_field' => '<p class="comment-form-comment">
        <label for="comment">' . _x('Comment', 'noun') . '</label>
        <textarea id="comment" name="comment" rows="5" required></textarea>
    </p>',
    'fields' => [
        'author' => '<p class="comment-form-author">
            <label for="author">' . ('Name') . ($req ? ' <span class="required">*</span>' : '') . '</label>
            <input id="author" name="author" type="text" value="' . esc_attr($commenter['comment_author']) . '"' . $aria_req . '>
        </p>',
        'email' => '<p class="comment-form-email">
            <label for="email">' . ('Email') . ($req ? ' <span class="required">*</span>' : '') . '</label>
            <input id="email" name="email" type="email" value="' . esc_attr($commenter['comment_author_email']) . '"' . $aria_req . '>
        </p>',
    ],
];

comment_form($args);

Step 13: Display Comments

To display comments below a post, WordPress provides wp_list_comments(). Inside your comments.php, after the form:

<?php if (have_comments()) : ?>
    <h2 class="comments-title">
        <?php
        $count = get_comments_number();
        printf(_n('%s Comment', '%s Comments', $count), number_format_i18n($count));
        ?>
    </h2>

    <ol class="comment-list">
        <?php
        wp_list_comments([
            'style' => 'ol',
            'short_ping' => true,
            'avatar_size' => 60,
        ]);
        ?>
    </ol>

    <?php the_comments_pagination(); ?>
<?php endif; ?>

Step 14: Add Widget Areas (Sidebar + Recent Posts)

You already registered the sidebar in functions.php (Step 8). Now display it. Create sidebar.php:

<aside class="sidebar">
    <?php if (is_active_sidebar('sidebar_widget')) : ?>
        <?php dynamic_sidebar('sidebar_widget'); ?>
    <?php else : ?>
        <p>No widgets added yet.</p>
    <?php endif; ?>
</aside>

Then call <?php get_sidebar(); ?> wherever you want it (typically in single.php and index.php).

In Admin → Appearance → Widgets you can now drag in:
– Categories widget → displays category list
– Tag Cloud widget → tags
– Recent Posts widget → 5 most recent posts
– Search widget → site search

All work automatically with the sidebar location you registered.

Displaying categories, tags, recent posts manually

If you want them in custom locations (not in a widget area):

<!-- Categories -->
<ul class="categories-list">
    <?php
    $categories = get_categories(['hide_empty' => true]);
    foreach ($categories as $category) :
        echo '<li><a href="' . esc_url(get_category_link($category->term_id)) . '">'
             . esc_html($category->name) . ' (' . $category->count . ')</a></li>';
    endforeach;
    ?>
</ul>

<!-- Tags -->
<div class="tags-cloud">
    <?php
    $tags = get_tags(['hide_empty' => true]);
    foreach ($tags as $tag) :
        echo '<a href="' . esc_url(get_tag_link($tag->term_id)) . '">#' . esc_html($tag->name) . '</a> ';
    endforeach;
    ?>
</div>

<!-- Recent Posts -->
<ul class="recent-posts">
    <?php
    $recent = new WP_Query(['posts_per_page' => 5, 'post_status' => 'publish']);
    while ($recent->have_posts()) : $recent->the_post();
        ?>
        <li>
            <a href="<?php the_permalink(); ?>">
                <?php the_title(); ?>
                <small><?php echo get_the_date(); ?></small>
            </a>
        </li>
    <?php endwhile;
    wp_reset_postdata();
    ?>
</ul>

Step 15: Custom Media Plugin (Advanced)

For the advanced section of the video, you build a Custom Media Plugin that uploads files, stores them in a custom database table, AND creates WordPress attachments. Here’s the core pattern:

Create wp-content/plugins/custom-media-plugin/main.php:

<?php
/*
Plugin Name: Custom Media Plugin
Description: Custom file uploads with both custom table tracking and WP attachment.
Version: 1.0.0
*/

register_activation_hook(FILE, 'cmp_activate');

function cmp_activate() {
    global $wpdb;
    $table_name = $wpdb->prefix . 'custom_media';

    $charset_collate = $wpdb->get_charset_collate();
    $sql = "CREATE TABLE $table_name (
        id INT NOT NULL AUTO_INCREMENT,
        file_name VARCHAR(255) NOT NULL,
        upload_date DATETIME NOT NULL,
        PRIMARY KEY (id)
    ) $charset_collate;";

    // Run the SQL via dbDelta
    require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    dbDelta($sql);
}

function cmp_handle_upload($uploaded_file) {
    global $wpdb;
    $table_name = $wpdb->prefix . 'custom_media';
    $uploaded_file_name = sanitize_file_name($uploaded_file['name']);

    // Insert into custom table
    $wpdb->insert(
        $table_name,
        [
            'file_name' => '/users_data/' . $uploaded_file_name,
            'upload_date' => current_time('mysql'),
        ]
    );

    // Also create a WP attachment so Media Library shows it
    $attachment = [
        'post_mime_type' => $uploaded_file['type'],
        'post_title' => sanitize_file_name($uploaded_file_name),
        'post_content' => '',
        'post_status' => 'inherit',
    ];

    $attach_id = wp_insert_attachment($attachment, $uploaded_file['file']);

    // Generate metadata + thumbnails
    require_once ABSPATH . 'wp-admin/includes/image.php';
    $attach_data = wp_generate_attachment_metadata($attach_id, $uploaded_file['file']);
    wp_update_attachment_metadata($attach_id, $attach_data);

    return $attach_id;
}

This pattern (custom DB table + WP attachment) is how plugins like WooCommerce, Gravity Forms, etc. integrate with WordPress media while keeping their own data structure.

Essential WordPress Template Tags Cheat Sheet

After this guide you’ll use these constantly. Bookmark this section:

Loop & Content

Function What it does
have_posts() Returns true if there are posts to display
the_post() Sets up the current post; required inside the loop
the_title() Echoes the post title
the_content() Echoes the full post content
the_excerpt() Echoes the post excerpt
the_permalink() Echoes the post URL
the_post_thumbnail('size') Echoes the featured image
get_the_ID() Returns the current post ID
get_post_meta($id, 'key', true) Returns a custom field value

Site Info

Function What it does
bloginfo('name') Site name
bloginfo('description') Tagline
bloginfo('charset') Character set
home_url() Returns the homepage URL
get_template_directory_uri() URL to current theme folder
get_stylesheet_uri() URL to theme’s style.css

Template Includes

Function What it does
get_header() Loads header.php
get_footer() Loads footer.php
get_sidebar() Loads sidebar.php
get_template_part('content', 'single') Loads content-single.php
comments_template() Loads comments.php

Navigation

Function What it does
wp_nav_menu($args) Renders a registered menu
wp_list_pages() Lists all pages
the_posts_pagination() Numbered pagination for archives
previous_post_link() / next_post_link() Single post navigation

Header / Footer Hooks (REQUIRED)

Function Where Why critical
wp_head() Before </head> Plugins inject CSS/JS here
wp_footer() Before </body> jQuery & plugin scripts inject here
body_class() On <body> Adds page-specific CSS classes
language_attributes() On <html> Sets lang attribute

Skip these and 90% of plugins will break.

Common Errors and Fixes

Error Cause Fix
Theme doesn’t show in Appearance → Themes Missing style.css or no header comment Add Theme Name: comment to style.css
Blank white page PHP syntax error Enable debug: add define('WP_DEBUG', true); to wp-config.php
CSS/JS not loading Hard-coded paths Use get_template_directory_uri() for all asset URLs
Menu not showing Menu not assigned to location Appearance → Menus → “Display Location” → check Primary
wp_head() missing → broken plugins You removed it from header.php Add <?php wp_head(); ?> before </head>
wp_footer() missing → broken admin bar / jQuery Missing from footer Add <?php wp_footer(); ?> before </body>
Custom post type missing MU plugin not loading Confirm file is in wp-content/mu-plugins/, not plugins/
Featured image options missing Theme doesn’t support thumbnails Add add_theme_support('post-thumbnails'); to functions.php
Pagination shows on every page Not wrapped in archive check Wrap with if (is_home() \|\| is_archive())
Comments don’t appear comments_template() not called Add comments_template(); to single.php
“Stylesheet missing” error Folder name vs style.css mismatch Make sure theme folder has style.css with valid header

Theme Development Workflow (Daily)

Once your theme is set up, the daily flow is fast:

# Terminal 1
cd wp-content/themes/my-theme
npm run build -- --watch    # Tailwind rebuild on save

# Terminal 2
npm run start               # BrowserSync live reload

# Now just edit .php files in your editor — changes appear instantly

File-to-template-hierarchy quick reference:

To edit… Edit this file
Homepage front-page.php
Blog listing index.php (or home.php)
Single blog post single.php
Static page page.php
Category archive category.php
Tag archive tag.php
404 page 404.php
Search results search.php
Header (every page) header.php
Footer (every page) footer.php
Sidebar sidebar.php
Comments comments.php
Custom logic / hooks functions.php

Pro Tips for Production-Ready Themes

  1. Always escape output — Use esc_html(), esc_url(), esc_attr() to prevent XSS attacks
  2. Always sanitize input — Use sanitize_text_field(), sanitize_email(), etc.
  3. Use nonces for forms — wp_nonce_field() + check_admin_referer()
  4. Internationalization — Wrap all text in ('Text', 'text-domain') for translation
  5. Use child themes — Never modify a parent theme directly
  6. Test with debug mode ON — Add define('WP_DEBUG', true); while developing
  7. Validate against the WordPress Theme Check plugin before shipping
  8. Performance — Use Redis for object caching (the video shows benchmarks!)

Wrapping Up

You now know how to build a complete WordPress theme from scratch:

  • ✅ Theme folder structure and required files
  • ✅ Converting any HTML template to a working WordPress theme
  • ✅ Custom post types via Must-Use Plugins
  • ✅ Tailwind CSS with BrowserSync live-reload
  • ✅ Dynamic menus, widgets, sidebars
  • ✅ Blog, single post, comments with custom form
  • ✅ Categories, tags, recent posts
  • ✅ A custom media plugin with its own database table

This is the same foundation used to build commercial themes on ThemeForest, agency projects, and custom client work. Every concept you learned scales — from a simple blog theme to a complex multi-page corporate site.

📺 Reminder: The full 2h 25m video walkthrough is at https://youtu.be/6RS92BJeREI — including the Redis performance test results (1:35:17) and Custom Media Plugin (2:16:45) which couldn’t fit completely in this written guide.

Next steps to deepen your skills

  • Build a child theme of your custom theme to practice safe customization
  • Add WooCommerce support — add_theme_support('woocommerce'); opens up e-commerce
  • Try Gutenberg block development — modern themes increasingly use blocks
  • Submit your theme to WordPress.org — free distribution, real-world testing

Related reading on this blog: wsl-docker-php-setup, [useful-docker-commands], [important-linux-commands-beginners], [git-for-teams-complete-guide]

Discussion

Be the first to comment

Leave a comment

Get a quote