WordPress Docs Page Template

9 min read Updated October 22, 2026
WordPress Docs Page Template
I had a reference page I really liked. Plain HTML, one file, sitting on my hard drive next to a pile of old builder kits. Dark sidebar on the left, search box up top, links grouped into sections. It felt like a help file from a real software company. Then I moved it into WordPress, and the first version came out boxed in and centered, with the sidebar floating in the middle of the page like it got lost on the way to work. Not the vibe. So I rebuilt it as a proper WordPress page template: full width, sidebar locked under the header, live search, and print styles. I’m going to use this same layout for every help and docs page I build from here on, so it was worth doing right. The code below is simplified so you can drop it into your own theme and make it yours.

Why a page template and not a page builder?

A page builder could get you close. But a page template is one PHP file you control completely. No extra plugin, no shortcode soup, no surprises when the builder pushes an update. If you can write HTML and CSS, you can write a page template. The PHP part is smaller than you’d think.

Step 1: Create the page template

A page template is just a PHP file in your theme folder with a special comment at the top. WordPress reads that comment and adds your template to the Template dropdown in the page editor.
<?php
/**
 * Template Name: Docs Page
 */

get_header();
?>

<main class="docs">
    <nav class="docs-nav">
        <!-- sidebar links go here -->
    </nav>

    <div class="docs-main">
        <h1><?php the_title(); ?></h1>
        <!-- content goes here -->
    </div>
</main>

<?php
get_footer();
Save it as page-docs.php in your theme’s root folder, the same place functions.php lives. Create a new page, open the Template dropdown in the sidebar, and pick Docs Page. [Screenshot: the Template dropdown in the page editor]

Step 2: Load the CSS only on that page

You could dump all of this into style.css, but then every page on your site downloads styles it never uses. The better move is a separate stylesheet that only loads when the docs template is in use. This goes in functions.php:
add_action( 'wp_enqueue_scripts', 'mytheme_docs_assets' );

function mytheme_docs_assets() {
    if ( ! is_page_template( 'page-docs.php' ) ) {
        return; // not a docs page, load nothing
    }

    $file = get_theme_file_path( 'assets/css/docs.css' );

    wp_enqueue_style(
        'mytheme-docs',
        get_theme_file_uri( 'assets/css/docs.css' ),
        array(),
        filemtime( $file ) // version = the last time you saved the file
    );
}
    • is_page_template() checks which template the current page is using.
    • get_theme_file_path() gives you the file’s location on the server (for PHP). get_theme_file_uri() gives you its web address (for the browser). Easy to mix up, so it’s worth remembering which is which.
    • filemtime() is my favorite trick here. It uses the file’s last-saved time as the version number, so every time you save your CSS, browsers grab the fresh copy. No more “did you clear your cache?”

Step 3: Go full width with CSS Grid

The boxed look came from wrapping everything in a centered container. For docs, the sidebar should hug the left edge of the screen. Two columns, and one line of CSS does most of the work:
.docs {
    display: grid;
    grid-template-columns: 300px minmax(0, 1fr);
}

.docs-main {
    padding: 40px 56px;
    max-width: 960px;
}

@media (max-width: 900px) {
    .docs {
        grid-template-columns: 1fr; /* stack on phones */
    }
}
300px is the sidebar. minmax(0, 1fr) means “take whatever space is left.” That 0 matters more than it looks: without it, a long URL or a wide code block can push the column wider than the screen, and suddenly your page scrolls sideways on a phone. [Screenshot: before (boxed) vs. after (full width)]

Step 4: The sticky sidebar (and the admin bar gotcha)

This is the part that makes it feel like a real help file. The sidebar stays put while you scroll through the content.
.docs-nav {
    position: sticky;
    top: 0;
    align-self: start;
    height: 100vh;
    overflow-y: auto;
}

/* Logged in? Make room for the admin bar. */
.admin-bar .docs-nav {
    top: 32px;
    height: calc(100vh - 32px);
}
position: sticky is the trick. The sidebar scrolls normally until it reaches the top of the screen, then it locks in place. align-self: start is the line people forget. Without it, the grid stretches the sidebar to the full height of the content, and a sidebar that’s already as tall as the page has nowhere to stick. Now the gotcha. When you’re logged in, WordPress pins its black admin bar across the top of the screen, and my sidebar slid right underneath it. Luckily, WordPress adds an admin-bar class to the body tag whenever that bar is showing. So you can push the sidebar down 32px for logged-in users only, and visitors never know the difference.

Step 5: Keep your links in an array, not in HTML

My original HTML file had 80-something links typed out by hand. In WordPress, I moved them into a PHP array and let a loop write the HTML for me. Adding a link now means adding one line.
<?php
$sections = array(
    'getting-started' => array(
        'title' => 'Getting Started',
        'links' => array(
            array( 'Theme Handbook', 'https://developer.wordpress.org/themes/' ),
            array( 'Plugin Handbook', 'https://developer.wordpress.org/plugins/' ),
        ),
    ),
    'tools' => array(
        'title' => 'Tools',
        'links' => array(
            array( 'Can I Use', 'https://caniuse.com' ),
            array( 'MDN Web Docs', 'https://developer.mozilla.org' ),
        ),
    ),
);
?>

<?php foreach ( $sections as $id => $section ) : ?>
    <section id="<?php echo esc_attr( $id ); ?>">
        <h2><?php echo esc_html( $section['title'] ); ?></h2>
        <ul>
            <?php foreach ( $section['links'] as $link ) : ?>
                <li>
                    <a href="<?php echo esc_url( $link[1] ); ?>" target="_blank" rel="noopener">
                        <?php echo esc_html( $link[0] ); ?>
                    </a>
                </li>
            <?php endforeach; ?>
        </ul>
    </section>
<?php endforeach; ?>
Loop over the same array a second time to build the sidebar, and the sidebar and the content can never get out of sync. Those esc_ functions are WordPress’s way of cleaning text before it hits the page: esc_html() for regular text, esc_url() for links, and esc_attr() for anything inside an HTML attribute. Your own array is probably safe. But escaping everything is a habit worth building now, before the day you’re printing something that came from a form.

Step 6: A little JavaScript for live search

const search = document.querySelector( '#docs-search' );

search.addEventListener( 'input', () => {
    const term = search.value.toLowerCase();

    document.querySelectorAll( '.docs-main li' ).forEach( ( item ) => {
        item.hidden = ! item.textContent.toLowerCase().includes( term );
    } );
} );
Type a word and anything that doesn’t match disappears. The input event fires on every keystroke, paste and delete, which beats the older keyup approach. Load the file with wp_enqueue_script() the same way as the CSS, and pass true as the last argument so it loads in the footer. In my version, the sidebar also highlights whichever section you’re reading as you scroll. That one gets its own post.

Step 7: Print styles

Reference pages get printed more than you’d think. This hides the sidebar and header when printing, and writes each link’s URL after it. A link on paper is useless if you can’t see where it goes.
@media print {
    .docs-nav,
    .site-header,
    .site-footer {
        display: none;
    }

    .docs {
        grid-template-columns: 1fr;
    }

    .docs-main a::after {
        content: " (" attr(href) ")";
        font-size: 0.8em;
    }
}

The mistake that took my whole site down

I’d be lying if I said this went smoothly. I unzipped my files into a subfolder inside my theme, added one require line to functions.php, hit refresh, and got a white screen. Not just on the new page. The whole site. The line looked like this:
require get_theme_file_path( 'inc/docs.php' );
get_theme_file_path() always starts from the theme’s root folder. My file was sitting in a subfolder one level deeper, so WordPress went looking in the wrong place and found nothing. And require does exactly one thing when a file is missing: it stops everything. Fatal error. The fix was moving the files to where the path said they’d be. But while you’re testing, guard the include so a missing file can’t take your site down with it:
$docs_file = get_theme_file_path( 'inc/docs.php' );

if ( file_exists( $docs_file ) ) {
    require_once $docs_file;
}
And if the white screen does get you, don’t panic. Connect with FTP or your host’s file manager, remove the line you just added to functions.php, and the site comes right back.

What’s Next

This layout is going to be the base for every help page and plugin doc I build. Here’s where it goes from here:
    • A general Docs template that builds the sidebar automatically from the H2 headings in the editor, so a new help page is just writing.
    • Sidebar highlighting that follows you as you scroll, using IntersectionObserver.
    • A back-to-top button for the really long pages.
Quick recap: a page template is one PHP file with a Template Name comment. Load assets only where they’re needed. CSS Grid handles the layout, position: sticky handles the sidebar, and you should always guard your require lines while testing.

Resources

Following along as I build out plugins and docs? I post updates on X and LinkedIn.
🧡 Spread the love
Zach Wheat
Written by

Zach Wheat

Zach Wheat is a designer, developer, and digital builder rooted in Hanford, CA — the heart of California's Central Valley. He's the founder of 559 Digital and the creator of the 559 Network, a locally-owned alternative to platforms like Craigslist built to serve the Valley.

A U.S. Navy veteran with 15+ years of self-taught experience in graphic design, WordPress development, and digital strategy — Zach doesn't chase trends. He builds things that last.

Work with Zach →   ·   X / Twitter   ·   Facebook

// Leave a Comment

Your email address will not be published. Required fields are marked *