HomebaseWorks
Lesson 4 of 4

Custom post types, taxonomies and WP_Query

Register a content type and its categories in a plugin, give them templates, and list them with a correct query loop.

EWritten by the HomebaseWorks editorial team · reviewed 26 Oct 2026

Posts and pages cover a blog, but a real site needs jobs, products, events or team members with their own fields and listings. Registering a custom post type is the standard way to add one, and WP_Query is how you list it anywhere. Both appear in practical WordPress screenings.

Advertisement

Registering the post type and a taxonomy

Put this in a small plugin, not the theme, so the content stays available if the theme changes.

<?php
add_action( 'init', 'hbw_register_job_type' );
function hbw_register_job_type() {
    register_post_type( 'job', array(
        'labels'       => array( 'name' => 'Jobs', 'singular_name' => 'Job' ),
        'public'       => true,
        'has_archive'  => true,
        'show_in_rest' => true,
        'menu_icon'    => 'dashicons-businessman',
        'supports'     => array( 'title', 'editor', 'thumbnail', 'excerpt' ),
        'rewrite'      => array( 'slug' => 'jobs' ),
    ) );

    register_taxonomy( 'job_location', 'job', array(
        'labels'       => array( 'name' => 'Locations', 'singular_name' => 'Location' ),
        'hierarchical' => true,
        'show_in_rest' => true,
        'rewrite'      => array( 'slug' => 'location' ),
    ) );
}

show_in_rest is what makes the block editor appear; without it you get the old classic editor. has_archive gives you a listing at /jobs/. After registering anything with a rewrite slug, open Settings, then Permalinks, and save once to refresh the URL rules, or the new pages return 404.

Templates for the new type

The template hierarchy extends naturally: archive-job.php for the listing, single-job.php for one job, taxonomy-job_location.php for one location. If none exists, WordPress falls back to archive.php, single.php and index.php.

Listing with WP_Query

Here is the loop that lists the five newest remote jobs on any page or widget:

$jobs = new WP_Query( array(
    'post_type'      => 'job',
    'posts_per_page' => 5,
    'orderby'        => 'date',
    'order'          => 'DESC',
    'tax_query'      => array(
        array(
            'taxonomy' => 'job_location',
            'field'    => 'slug',
            'terms'    => 'remote',
        ),
    ),
) );

if ( $jobs->have_posts() ) {
    while ( $jobs->have_posts() ) {
        $jobs->the_post();
        echo '<h3><a href="' . esc_url( get_permalink() ) . '">'
            . esc_html( get_the_title() ) . '</a></h3>';
    }
    wp_reset_postdata();
}

wp_reset_postdata() restores the main query so the rest of the page is not confused by your loop. Escape every value you print: esc_html for text, esc_url for links, esc_attr inside attributes. Extra data such as salary range or closing date goes in post meta, read with get_post_meta( get_the_ID(), 'closing_date', true ).

Common mistakes and a practice habit

  • Setting public to false and then wondering why the archive and single pages do not exist.
  • Forgetting to refresh permalinks after registration.
  • Using query_posts(), which replaces the main query and breaks pagination. Use WP_Query or the pre_get_posts action.
  • Choosing a post type key that is reserved or over 20 characters, such as type or post.

Practice habit: register an event post type with a date meta field, then write a WP_Query that lists only future events sorted by that date using meta_key and orderby => 'meta_value'. Check the result against the admin list.

Advertisement
Continue with WordPress4 lessons · 1 interview set · 1 test · 1 certificate
Not tested

More in Web and app