📖 WordPress Development - Theme Development
80 phút

Theme Development

Template Hierarchy

Front Page:
  front-page.php → home.php → index.php

Single Post:
  single-post.php → single.php → singular.php → index.php

Page:
  page-{slug}.php → page-{id}.php → page.php → singular.php → index.php

Category:
  category-{slug}.php → category-{id}.php → category.php → archive.php → index.php

Tag, Author, Date, Custom Post Type
  Similar hierarchy to category

Cấu trúc theme cơ bản

my-theme/
├── style.css              # Required
├── functions.php
├── index.php              # Required
├── header.php
├── footer.php
├── sidebar.php
├── single.php
├── page.php
├── archive.php
├── search.php
├── 404.php
├── screenshot.png
├── assets/
│   ├── css/
│   ├── js/
│   └── images/
├── template-parts/
│   ├── content.php
│   ├── content-single.php
│   └── content-page.php
└── inc/
    ├── customizer.php
    ├── template-tags.php
    └── custom-functions.php

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">
    <div class="container">
        <div class="site-branding">
            <?php if (has_custom_logo()): ?>
                <?php the_custom_logo(); ?>
            <?php else: ?>
                <a href="<?php echo esc_url(home_url('/')); ?>">
                    <?php bloginfo('name'); ?>
                </a>
            <?php endif; ?>
        </div>

        <nav class="site-nav">
            <?php
            wp_nav_menu([
                'theme_location' => 'primary',
                'container'      => false,
                'menu_class'     => 'menu',
                'fallback_cb'    => false,
            ]);
            ?>
        </nav>
    </div>
</header>

footer.php

<footer class="site-footer">
    <div class="container">
        <p>&copy; <?php echo date('Y'); ?> <?php bloginfo('name'); ?>. 
           <?php esc_html_e('All rights reserved.', 'my-theme'); ?>
        </p>

        <?php
        wp_nav_menu([
            'theme_location' => 'footer',
            'container'      => false,
            'menu_class'     => 'footer-menu',
            'depth'          => 1,
        ]);
        ?>
    </div>
</footer>

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

index.php

<?php get_header(); ?>

<main class="site-main container">
    <?php if (have_posts()): ?>
        <div class="posts-grid">
            <?php while (have_posts()): the_post(); ?>
                <?php get_template_part('template-parts/content', get_post_type()); ?>
            <?php endwhile; ?>
        </div>

        <?php
        the_posts_pagination([
            'mid_size'  => 2,
            'prev_text' => __('&laquo; Previous', 'my-theme'),
            'next_text' => __('Next &raquo;', 'my-theme'),
        ]);
        ?>
    <?php else: ?>
        <p><?php esc_html_e('No posts found.', 'my-theme'); ?></p>
    <?php endif; ?>
</main>

<?php get_footer(); ?>

template-parts/content.php

<article id="post-<?php the_ID(); ?>" <?php post_class('post-card'); ?>>
    <?php if (has_post_thumbnail()): ?>
        <a href="<?php the_permalink(); ?>" class="post-thumbnail">
            <?php the_post_thumbnail('medium_large'); ?>
        </a>
    <?php endif; ?>

    <div class="post-content">
        <header class="entry-header">
            <?php the_title('<h2 class="entry-title"><a href="' . esc_url(get_permalink()) . '">', '</a></h2>'); ?>

            <div class="entry-meta">
                <time datetime="<?php echo esc_attr(get_the_date('c')); ?>">
                    <?php echo esc_html(get_the_date()); ?>
                </time>
                <span class="author">by <?php the_author_posts_link(); ?></span>
            </div>
        </header>

        <div class="entry-summary">
            <?php the_excerpt(); ?>
        </div>

        <a href="<?php the_permalink(); ?>" class="read-more">
            <?php esc_html_e('Read more', 'my-theme'); ?> &rarr;
        </a>
    </div>
</article>

single.php

<?php get_header(); ?>

<main class="site-main container">
    <?php while (have_posts()): the_post(); ?>
        <article id="post-<?php the_ID(); ?>" <?php post_class('single-post'); ?>>
            <header class="entry-header">
                <?php the_title('<h1 class="entry-title">', '</h1>'); ?>
                
                <div class="entry-meta">
                    <time datetime="<?php echo esc_attr(get_the_date('c')); ?>">
                        <?php echo esc_html(get_the_date()); ?>
                    </time>
                    <span>by <?php the_author_posts_link(); ?></span>
                    
                    <?php if (has_category()): ?>
                        <span class="categories">
                            <?php the_category(', '); ?>
                        </span>
                    <?php endif; ?>
                </div>
            </header>

            <?php if (has_post_thumbnail()): ?>
                <div class="post-thumbnail">
                    <?php the_post_thumbnail('large'); ?>
                </div>
            <?php endif; ?>

            <div class="entry-content">
                <?php the_content(); ?>
                
                <?php
                wp_link_pages([
                    'before' => '<nav class="page-links">' . __('Pages:', 'my-theme'),
                    'after'  => '</nav>',
                ]);
                ?>
            </div>

            <footer class="entry-footer">
                <?php the_tags('<div class="tags">', ', ', '</div>'); ?>
            </footer>
        </article>

        <?php
        the_post_navigation([
            'prev_text' => '<span class="nav-label">Previous</span><span class="nav-title">%title</span>',
            'next_text' => '<span class="nav-label">Next</span><span class="nav-title">%title</span>',
        ]);

        if (comments_open() || get_comments_number()) {
            comments_template();
        }
        ?>
    <?php endwhile; ?>
</main>

<?php get_footer(); ?>

Functions.php essentials

<?php

// Prevent direct access
if (!defined('ABSPATH')) exit;

// Theme constants
define('MY_THEME_VERSION', '1.0.0');
define('MY_THEME_DIR', get_template_directory());
define('MY_THEME_URI', get_template_directory_uri());

// Include files
require_once MY_THEME_DIR . '/inc/template-tags.php';
require_once MY_THEME_DIR . '/inc/customizer.php';

/**
 * Theme setup
 */
function my_theme_setup() {
    load_theme_textdomain('my-theme', MY_THEME_DIR . '/languages');

    add_theme_support('title-tag');
    add_theme_support('post-thumbnails');
    add_theme_support('custom-logo', [
        'height'      => 100,
        'width'       => 400,
        'flex-height' => true,
        'flex-width'  => true,
    ]);
    add_theme_support('html5', ['search-form', 'comment-form', 'comment-list', 'gallery', 'caption', 'style', 'script']);
    add_theme_support('responsive-embeds');
    add_theme_support('align-wide');
    add_theme_support('editor-styles');

    // Image sizes
    add_image_size('my-theme-featured', 1200, 600, true);
    add_image_size('my-theme-thumbnail', 600, 400, true);

    register_nav_menus([
        'primary' => __('Primary Menu', 'my-theme'),
        'footer'  => __('Footer Menu', 'my-theme'),
    ]);
}
add_action('after_setup_theme', 'my_theme_setup');

/**
 * Enqueue scripts and styles
 */
function my_theme_scripts() {
    wp_enqueue_style(
        'my-theme-style',
        get_stylesheet_uri(),
        [],
        MY_THEME_VERSION
    );

    wp_enqueue_style(
        'my-theme-main',
        MY_THEME_URI . '/assets/css/main.css',
        ['my-theme-style'],
        MY_THEME_VERSION
    );

    wp_enqueue_script(
        'my-theme-script',
        MY_THEME_URI . '/assets/js/main.js',
        [],
        MY_THEME_VERSION,
        true
    );

    if (is_singular() && comments_open() && get_option('thread_comments')) {
        wp_enqueue_script('comment-reply');
    }
}
add_action('wp_enqueue_scripts', 'my_theme_scripts');

/**
 * Register widget areas
 */
function my_theme_widgets_init() {
    register_sidebar([
        'name'          => __('Sidebar', 'my-theme'),
        'id'            => 'sidebar-1',
        'description'   => __('Add widgets here.', 'my-theme'),
        'before_widget' => '<section id="%1$s" class="widget %2$s">',
        'after_widget'  => '</section>',
        'before_title'  => '<h2 class="widget-title">',
        'after_title'   => '</h2>',
    ]);

    register_sidebar([
        'name'          => __('Footer Column 1', 'my-theme'),
        'id'            => 'footer-1',
        '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', 'my_theme_widgets_init');

Template tags

<?php // app/inc/template-tags.php ?>

/**
 * Display post meta
 */
function my_theme_post_meta() {
    printf(
        '<div class="entry-meta">
            <time datetime="%1$s">%2$s</time>
            <span class="author">%3$s</span>
        </div>',
        esc_attr(get_the_date('c')),
        esc_html(get_the_date()),
        sprintf(
            '<a href="%1$s">%2$s</a>',
            esc_url(get_author_posts_url(get_the_author_meta('ID'))),
            esc_html(get_the_author())
        )
    );
}

/**
 * Get the post thumbnail URL
 */
function my_theme_get_thumbnail_url($size = 'full') {
    if (!has_post_thumbnail()) {
        return get_theme_file_uri('/assets/images/placeholder.jpg');
    }
    return get_the_post_thumbnail_url(get_the_ID(), $size);
}

/**
 * Breadcrumbs
 */
function my_theme_breadcrumbs() {
    if (is_front_page()) return;

    echo '<nav class="breadcrumbs" aria-label="Breadcrumb"><ol>';
    echo '<li><a href="' . esc_url(home_url('/')) . '">Home</a></li>';

    if (is_single()) {
        $categories = get_the_category();
        if (!empty($categories)) {
            echo '<li><a href="' . esc_url(get_category_link($categories[0]->term_id)) . '">'
                . esc_html($categories[0]->name) . '</a></li>';
        }
        echo '<li aria-current="page">' . esc_html(get_the_title()) . '</li>';
    } elseif (is_page()) {
        echo '<li aria-current="page">' . esc_html(get_the_title()) . '</li>';
    } elseif (is_category()) {
        echo '<li aria-current="page">' . esc_html(single_cat_title('', false)) . '</li>';
    }

    echo '</ol></nav>';
}

Custom Post Types và Taxonomies

/**
 * Register custom post type: Portfolio
 */
function my_theme_register_cpt() {
    register_post_type('portfolio', [
        'labels' => [
            'name'          => __('Portfolio', 'my-theme'),
            'singular_name' => __('Project', 'my-theme'),
            'add_new'       => __('Add New', 'my-theme'),
            'add_new_item'  => __('Add New Project', 'my-theme'),
            'edit_item'     => __('Edit Project', 'my-theme'),
            'view_item'     => __('View Project', 'my-theme'),
            'search_items'  => __('Search Projects', 'my-theme'),
            'not_found'     => __('No projects found', 'my-theme'),
        ],
        'public'        => true,
        'has_archive'   => true,
        'menu_icon'     => 'dashicons-portfolio',
        'menu_position' => 20,
        'supports'      => ['title', 'editor', 'thumbnail', 'excerpt', 'custom-fields'],
        'rewrite'       => ['slug' => 'projects'],
        'show_in_rest'  => true, // Gutenberg support
    ]);

    register_taxonomy('portfolio_category', 'portfolio', [
        'labels' => [
            'name'          => __('Project Categories', 'my-theme'),
            'singular_name' => __('Project Category', 'my-theme'),
        ],
        'hierarchical' => true,
        'show_in_rest' => true,
        'rewrite'      => ['slug' => 'project-category'],
    ]);

    register_taxonomy('portfolio_tag', 'portfolio', [
        'labels' => [
            'name'          => __('Project Tags', 'my-theme'),
            'singular_name' => __('Project Tag', 'my-theme'),
        ],
        'hierarchical' => false,
        'show_in_rest' => true,
    ]);
}
add_action('init', 'my_theme_register_cpt');

Customizer

<?php // app/inc/customizer.php ?>

function my_theme_customize_register($wp_customize) {
    // Add section
    $wp_customize->add_section('my_theme_options', [
        'title'    => __('Theme Options', 'my-theme'),
        'priority' => 30,
    ]);

    // Footer text setting
    $wp_customize->add_setting('footer_text', [
        'default'           => '© ' . date('Y') . ' ' . get_bloginfo('name'),
        'sanitize_callback' => 'sanitize_text_field',
        'transport'         => 'refresh',
    ]);

    $wp_customize->add_control('footer_text', [
        'label'   => __('Footer Text', 'my-theme'),
        'section' => 'my_theme_options',
        'type'    => 'text',
    ]);

    // Primary color
    $wp_customize->add_setting('primary_color', [
        'default'           => '#0073aa',
        'sanitize_callback' => 'sanitize_hex_color',
    ]);

    $wp_customize->add_control(
        new WP_Customize_Color_Control($wp_customize, 'primary_color', [
            'label'   => __('Primary Color', 'my-theme'),
            'section' => 'colors',
        ])
    );
}
add_action('customize_register', 'my_theme_customize_register');

/**
 * Output customizer CSS
 */
function my_theme_customizer_css() {
    $primary = get_theme_mod('primary_color', '#0073aa');
    ?>
    <style>
        :root {
            --primary-color: <?php echo esc_attr($primary); ?>;
        }
        a, button, .button {
            color: var(--primary-color);
        }
    </style>
    <?php
}
add_action('wp_head', 'my_theme_customizer_css');

Bài tập thực hành

Hãy tạo complete theme với custom post type!

📝 Bài tập (1)

  1. Tạo theme với custom post type