Posts vs Pages

Posts and pages are both rows in wp_posts — hierarchy, taxonomies and archives are the only real differences.

Syntaxget_post_type( int|WP_Post|null $post = null ): string|false

WordPress registers two content types for you: post and page. Both are rows in the wp_posts table, both come back as WP_Post objects, and the post_type column is the only thing separating them. Every behavioural difference comes from the arguments core passed to register_post_type() for each one.

What actually differs

Posts are chronological. Core registers them with the category and post_tag taxonomies, they fill the blog index and the feeds, and date archives query them. Pages are hierarchical: they support page-attributes, so post_parent nests them into URLs like /about/team/ and menu_order sorts siblings. Pages carry no taxonomies and have no archive of their own, and they can be assigned a page template.

Choosing between them

Use a page when the content's place in the site is a position in a tree — About, Pricing, a nested policy section. Use a post when the item is one of many similar things ordered by time. If it is neither — products, events, staff profiles — it belongs in a custom post type instead of being forced into either.

What trips people up

In the loop, the_title() is the short form of echo get_the_title( get_the_ID() ); both run the the_title filter, the short one just echoes instead of returning. The other surprise is the front page: is_front_page() and is_home() are different tests, and get_pages() returns false outright if you point it at a non-hierarchical post type.

Example

Example · php
<?php
// Pages and posts are the same table; you query them differently because
// they were registered differently.

// Pages are hierarchical. get_pages() returns WP_Post[] (or false if the
// post type is not hierarchical). Defaults: post_type 'page',
// post_status 'publish', sort_column 'post_title'.
$children = get_pages( array(
    'child_of'    => get_the_ID(),
    'parent'      => get_the_ID(), // direct children only; child_of also returns grandchildren
    'sort_column' => 'menu_order', // the field the Page Attributes box writes
    'sort_order'  => 'ASC',
) );

foreach ( $children as $child ) {
    printf(
        '<li><a href="%s">%s</a></li>',
        esc_url( get_permalink( $child ) ),
        // get_the_title( $child ) is the long form of the_title() in the loop.
        esc_html( get_the_title( $child ) )
    );
}

// Posts are chronological. Same table, ordered by date instead of a tree.
$recent = new WP_Query( array(
    'post_type'      => 'post',
    'posts_per_page' => 5,
    'orderby'        => 'date',
    'order'          => 'DESC',
) );

while ( $recent->have_posts() ) {
    $recent->the_post();
    the_title( '<h2>', '</h2>' ); // shorthand for echo '<h2>' . get_the_title() . '</h2>'
}
wp_reset_postdata();

When to use it

  • Deciding whether a new "Case Studies" section should be pages, posts, or a custom post type
  • Listing the child pages of the current page in a sidebar navigation
  • Printing a heading on the blog index when the assigned Posts page's own content is discarded
  • Searching posts and pages in one query for a site-wide results template

More examples

Querying posts and pages together

Because both types live in wp_posts, a single WP_Query can return them together — useful for a site-wide search results template where you want static pages listed alongside articles, but need to render dates only for the chronological ones.

Example · php
// post_type accepts an array, so one query can span both built-in types.
$results = new WP_Query( array(
    'post_type'      => array( 'post', 'page' ),
    's'              => $search_term,
    'posts_per_page' => 20,
    'post_status'    => 'publish',
) );

while ( $results->have_posts() ) {
    $results->the_post();

    // Branch on the type rather than assuming one shape.
    if ( 'page' === get_post_type() ) {
        echo '<span>Page</span>';
    } else {
        echo '<span>' . esc_html( get_the_date() ) . '</span>';
    }

    the_title( '<h3>', '</h3>' );
}
wp_reset_postdata();

Telling the front page from the blog index

The single most common source of confusion between the two types. Reach for this when a client's static home page and blog listing need different headers, or when a theme prints the wrong title on the blog archive.

Example · php
// Settings > Reading writes two options when "A static page" is selected.
$front_page_id = (int) get_option( 'page_on_front' );  // 0 when the front page is the blog
$posts_page_id = (int) get_option( 'page_for_posts' ); // the page whose body is ignored

if ( 'page' === get_option( 'show_on_front' ) && $front_page_id ) {
    // A page row is being used as the home page.
    $front = get_post( $front_page_id );
}

// In a template, use the conditionals rather than the options:
// is_front_page() - whatever is at the site root, page or blog index
// is_home()       - the blog index, wherever it lives
if ( is_home() && ! is_front_page() ) {
    // We are on the assigned Posts page. Its post_content is never output;
    // print its title manually if the design needs one.
    echo '<h1>' . esc_html( get_the_title( $posts_page_id ) ) . '</h1>';
}

Building a page breadcrumb from post_parent

Shows the practical consequence of hierarchy: pages have an ancestor chain you can walk, posts do not. Use it in a page.php template, and note the is_post_type_hierarchical() guard so the function degrades quietly on posts.

Example · php
/**
 * Pages nest through post_parent; posts almost never do.
 * get_post_ancestors() returns ancestor IDs, closest parent first.
 */
function acme_page_breadcrumb( $post = null ) {
    $post = get_post( $post );

    if ( ! $post instanceof WP_Post || ! is_post_type_hierarchical( $post->post_type ) ) {
        return '';
    }

    $trail     = array();
    $ancestors = array_reverse( get_post_ancestors( $post ) ); // top level first

    foreach ( $ancestors as $ancestor_id ) {
        $trail[] = sprintf(
            '<a href="%s">%s</a>',
            esc_url( get_permalink( $ancestor_id ) ),
            esc_html( get_the_title( $ancestor_id ) )
        );
    }

    $trail[] = esc_html( get_the_title( $post ) );

    return implode( ' / ', $trail );
}

Discussion

  • Be the first to comment on this lesson.