Most WordPress developers waste significant time fighting the default page layout when a single PHP file would solve the problem in minutes. Custom page templates give you complete control over how individual pages render—independent of your active theme's default structure—and the technique has remained fundamentally unchanged since WordPress introduced the template system in version 1.5.
A page template is a PHP file placed in your theme directory that WordPress recognizes through a special file header. Once registered, it appears in the Page Attributes panel as a selectable option. Any page assigned to that template renders using your custom file rather than the default page.php. The result is surgical control over layout without touching the theme's core files.
This guide covers the classic PHP template approach applicable to all WordPress themes, followed by the block-based equivalent for Site Editor themes. Prerequisites: FTP or cPanel access to your theme folder, or a local development environment with SSH access.
Step 1: Identify the Correct Theme Directory
Templates must live in your active theme's directory. If you are working with a parent theme—or any theme you did not build yourself—create a child theme first. Placing custom templates in a parent theme means they will be overwritten during the next theme update.
The correct path is wp-content/themes/your-child-theme/. Confirm this by navigating to Appearance > Theme File Editor in the WordPress admin. The file browser on the right shows your active theme's directory structure. If you see style.css with a Template: line pointing to another theme, you are already working in a child theme. Good. If not, follow our child theme setup guide before proceeding.
Step 2: Create the Template File
Using your FTP client or code editor, create a new PHP file in the theme root directory. Naming convention matters for organisation but not functionality—use something descriptive like template-landing.php or template-full-width.php. Avoid spaces in the filename.
The file must begin with this exact comment block:
<?php
/**
* Template Name: Landing Page
* Template Post Type: page
*/
get_header(); ?>
The Template Name line is what WordPress reads to register the template. The string following the colon becomes the label shown in the Page Attributes dropdown. Template Post Type is optional and restricts availability to specific post types; omitting it defaults to pages only. According to the WordPress Developer Documentation, this header approach has been the canonical registration method since the Classic Theme era and remains valid in hybrid themes.
Step 3: Build the Template Structure
After the header, the template behaves like any standard WordPress theme file. You have access to the full WordPress template tag library and any functions registered in your theme's functions.php.
A minimal working template that removes the sidebar and outputs page content at full width:
<?php
/**
* Template Name: Full Width
* Template Post Type: page
*/
get_header(); ?>
<main id="primary" class="site-main full-width">
<?php while ( have_posts() ) : the_post(); ?>
<article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
<header class="entry-header">
<?php the_title( '<h1 class="entry-title">', '</h1>' ); ?>
</header>
<div class="entry-content">
<?php the_content(); ?>
</div>
</article>
<?php endwhile; ?>
</main>
<?php get_footer(); ?>
This structure follows the PHP template literal pattern and the WordPress Loop. The have_posts() / the_post() pattern is required even for single-page templates—WordPress does not automatically set up the query context unless you run the loop.
Step 4: Add Custom Markup and Styling
With the loop in place, add your custom HTML structure between get_header() and get_footer(). For a landing page template, a common pattern is a hero section above the loop and a feature grid below it:
get_header();
// Hero section — static or pulled from custom fields
$hero_heading = get_post_meta( get_the_ID(), '_hero_heading', true );
$hero_sub = get_post_meta( get_the_ID(), '_hero_sub', true );
?>
<section class="hero-section">
<h1><?php echo esc_html( $hero_heading ?: get_the_title() ); ?></h1>
<p><?php echo esc_html( $hero_sub ); ?></p>
</section>
<?php while ( have_posts() ) : the_post();
the_content();
endwhile; ?>
Note the use of esc_html() on all output. The OWASP XSS guidelines apply equally to WordPress templates—output escaping is mandatory on any variable or post meta value before echoing it into the DOM. The WordPress Security Handbook catalogs the full set of escaping functions: esc_html(), esc_url(), esc_attr(), and wp_kses_post() for HTML-containing fields.
Inline CSS is viable for one-off templates. For reusable templates, enqueue a dedicated stylesheet via wp_enqueue_style() in functions.php, conditioned on is_page_template('template-landing.php') to avoid loading it globally.
Step 5: Assign the Template to a Page
Upload the finished file to your child theme directory via FTP, or save it if you edited through the Theme File Editor. Navigate to Pages > any existing page (or create a new one) and open the Page Attributes panel in the right sidebar. The Template dropdown now includes your new template name.
Select it, publish or update the page, and visit it in the browser. If the layout does not reflect your template, the most common cause is a caching layer serving an old version. Clear your server cache, CDN cache (including Cloudflare if active), and browser cache. Then verify the correct template is still selected in Page Attributes—WordPress occasionally reverts the assignment if the template file has a syntax error on save.
Step 6: Block-Based Alternative for Site Editor Themes
For themes that use the Full Site Editor—including Twenty Twenty-Five and most modern block themes—the PHP file approach still works but is supplemented by a JSON-based template system. Block templates live in the /templates/ directory and use the .html extension containing serialized block markup.
Create templates/page-landing.html in your theme with a standard block template:
<!-- wp:template-part {"slug":"header","tagName":"header"} /-->
<main class="wp-block-group alignfull">
<!-- wp:post-content {"layout":{"type":"constrained"}} /-->
</main>
<!-- wp:template-part {"slug":"footer","tagName":"footer"} /-->
WordPress assigns this template to any page whose slug matches the filename prefix after page-. For dynamic assignment (assigning any page to this template regardless of slug), register it via theme.json under the customTemplates key. Our theme.json complete guide covers the full syntax.
Common Mistakes to Avoid
- Editing parent theme files directly. Updates wipe all modifications. Child theme is mandatory for any production template work.
- Missing get_header() and get_footer(). These calls load enqueued scripts, stylesheets, and the
<html>/<body>tags. Omitting them produces a broken, unstyled page. - Skipping output escaping. Post meta retrieved via
get_post_meta()is unsanitized. Always escape before output. - Using page slug in template filename. WordPress does not automatically apply
template-about.phpto the page with slugabout. The assignment is always manual via Page Attributes.
For deeper theme architecture decisions—particularly whether to use PHP templates, block templates, or theme parts—our Full Site Editor guide and the block themes comparison provide context for choosing the right approach for your project's scale and maintainability requirements.
Need a Theme Built for Customisation?
Our themes ship with documented template hooks, clean file structure, and child-theme-ready architecture. No reverse-engineering required.
Browse Themes