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.
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.
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
publicto 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. UseWP_Queryor thepre_get_postsaction. - Choosing a post type key that is reserved or over 20 characters, such as
typeorpost.
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.
More in Web and app