Snippets / WordPress

Project Category Archive Navigation with 4 Column Grid

Build a project archive with an All link, project_cat term navigation, active archive classes, and a responsive 4 column project grid using native WordPress archive queries.

PHP template SEO-aware archives ~8 min setup No plugin required Beginner-friendly WP 6.0+ · PHP 8.0+ Updated May 18, 2026
TL;DR

Build a filterable project grid with real category archive pages and a 4-column layout — no JavaScript, no plugin needed.

Use this when you want each project category to have its own URL that search engines can crawl, instead of a JavaScript filter that keeps everyone on the same page.

01 — Building it step by step

Building the project category archive navigation

5 steps from an empty file to a filtered project grid. The archive file grows by one piece each step — you can reload your browser after each one and see something new.

Build a project archive with category filter links in 5 steps. Start with just the archive file, then add one piece at a time until the 4-column grid with clickable category filters is working.

Step 1: Create the archive file. WordPress automatically loads archive-projects.php when someone visits /projects/. Create that file in your theme root. For now, just loop through posts and output the title — nothing else yet:

archive-projects.php
php
<?php
/**
 * File: archive-projects.php
 * WordPress loads this automatically when someone visits /projects/.
 */

get_header();
?>

<main>

    <?php while ( have_posts() ) : the_post(); ?>

        <h2>
            <a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
        </h2>

    <?php endwhile; ?>

</main>

<?php
get_footer();

Step 2: Declare your slugs as variables. You will use the post type slug and taxonomy slug in several places. Naming them once at the top means you only update one line if your slugs are different:

archive-projects.php
php
<?php
// Your CPT slug — matches the first argument in register_post_type().
$post_type = 'projects';

// Your taxonomy slug — matches the first argument in register_taxonomy().
$taxonomy = 'project_cat';

get_header();
?>

<main>

    <?php while ( have_posts() ) : the_post(); ?>

        <h2>
            <a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
        </h2>

    <?php endwhile; ?>

</main>

<?php
get_footer();

Step 3: Add the “All” link. get_post_type_archive_link() returns the archive URL. is_post_type_archive() returns true only when you are currently on that page — that is how PHP decides whether to add the is-active class:

archive-projects.php
php
<?php
$post_type   = 'projects';
$taxonomy    = 'project_cat';
$archive_url = get_post_type_archive_link( $post_type );
$on_all_page = is_post_type_archive( $post_type );

get_header();
?>

<main>

    <nav class="project-filter">

        <a href="<?php echo esc_url( $archive_url ); ?>"
           class="project-filter__link <?php echo $on_all_page ? 'is-active' : ''; ?>">
            All
        </a>

    </nav>

    <?php while ( have_posts() ) : the_post(); ?>

        <h2>
            <a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
        </h2>

    <?php endwhile; ?>

</main>

<?php
get_footer();

Step 4: Add one link per category. get_terms() fetches all categories for your taxonomy. get_queried_object() returns the term currently being viewed — so PHP can compare each term’s ID against it and add is-active to only the right link:

archive-projects.php
php
<?php
$post_type    = 'projects';
$taxonomy     = 'project_cat';
$archive_url  = get_post_type_archive_link( $post_type );
$on_all_page  = is_post_type_archive( $post_type );

$terms        = get_terms( [ 'taxonomy' => $taxonomy, 'hide_empty' => true ] );
$current_term = get_queried_object();
$on_term_page = is_tax( $taxonomy ) && $current_term instanceof WP_Term;

get_header();
?>

<main>

    <nav class="project-filter">

        <a href="<?php echo esc_url( $archive_url ); ?>"
           class="project-filter__link <?php echo $on_all_page ? 'is-active' : ''; ?>">
            All
        </a>

        <?php foreach ( $terms as $term ) : ?>
            <?php $is_active = $on_term_page && $current_term->term_id === $term->term_id; ?>
            <a href="<?php echo esc_url( get_term_link( $term ) ); ?>"
               class="project-filter__link <?php echo $is_active ? 'is-active' : ''; ?>">
                <?php echo esc_html( $term->name ); ?>
            </a>
        <?php endforeach; ?>

    </nav>

    <?php while ( have_posts() ) : the_post(); ?>

        <h2>
            <a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
        </h2>

    <?php endwhile; ?>

</main>

<?php
get_footer();

Step 5: Replace the title list with a card grid. One change inside the loop — wrap each post in an article with a thumbnail and excerpt. The nav above stays exactly the same:

archive-projects.php
php
<?php
$post_type    = 'projects';
$taxonomy     = 'project_cat';
$archive_url  = get_post_type_archive_link( $post_type );
$on_all_page  = is_post_type_archive( $post_type );

$terms        = get_terms( [ 'taxonomy' => $taxonomy, 'hide_empty' => true ] );
$current_term = get_queried_object();
$on_term_page = is_tax( $taxonomy ) && $current_term instanceof WP_Term;

get_header();
?>

<main>

    <nav class="project-filter">

        <a href="<?php echo esc_url( $archive_url ); ?>"
           class="project-filter__link <?php echo $on_all_page ? 'is-active' : ''; ?>"
           <?php echo $on_all_page ? 'aria-current="page"' : ''; ?>>
            All
        </a>

        <?php foreach ( $terms as $term ) : ?>
            <?php $is_active = $on_term_page && $current_term->term_id === $term->term_id; ?>
            <a href="<?php echo esc_url( get_term_link( $term ) ); ?>"
               class="project-filter__link <?php echo $is_active ? 'is-active' : ''; ?>"
               <?php echo $is_active ? 'aria-current="page"' : ''; ?>>
                <?php echo esc_html( $term->name ); ?>
            </a>
        <?php endforeach; ?>

    </nav>

    <div class="project-grid">

        <?php while ( have_posts() ) : the_post(); ?>

            <article class="project-card">

                <a class="project-card__media" href="<?php the_permalink(); ?>">
                    <?php if ( has_post_thumbnail() ) : ?>
                        <?php the_post_thumbnail( 'medium_large' ); ?>
                    <?php endif; ?>
                </a>

                <div class="project-card__body">
                    <h2 class="project-card__title">
                        <a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
                    </h2>
                    <?php if ( has_excerpt() ) : ?>
                        <p><?php echo esc_html( get_the_excerpt() ); ?></p>
                    <?php endif; ?>
                </div>

            </article>

        <?php endwhile; ?>

    </div>

</main>

<?php
get_footer();

That is your complete archive-projects.php. For category archive pages WordPress also needs taxonomy-project_cat.php. You can copy the same file there, or use get_template_part() in both files to share one template and keep a single copy of the code. Section 03 shows the shared template approach.

02 — How it works

How project category archive navigation works

The active class comes from WordPress conditional tags, not JavaScript.

1

Why the All link needs a different conditional than the category links

The All link points to the main post type archive, and the category links each point to a taxonomy term archive. WordPress treats these as two different types of pages. is_post_type_archive() returns true only on the main archive URL. is_tax() returns true only on a taxonomy term URL. Checking them separately lets you highlight exactly one link at a time — whichever page you are currently on.

2

Why each category gets its own URL instead of a JavaScript filter

A JavaScript filter hides and shows posts without changing the URL. That means every category shares the same page — visitors cannot bookmark a specific category, and search engines cannot index each category separately. Using get_term_link( $term ) gives each category its own real URL. WordPress handles the filtering automatically when it loads taxonomy-project_cat.php.

3

Why you should not replace the main query with a custom WP_Query

On archive and taxonomy templates, WordPress already runs the right query before your template file loads. On archive-projects.php it queries all published projects. On taxonomy-project_cat.php it automatically filters down to just the projects in the selected category. If you add a new WP_Query on top, two queries run — the default one is still there, just ignored. Sticking with have_posts() keeps pagination, Rank Math titles, and canonical URLs working correctly without any extra setup.

4

Why real category URLs are better for SEO than a JavaScript filter

When every category lives on the same URL, search engines see one page with mixed content and cannot tell the categories apart. With real taxonomy archive URLs, each category page such as /project-category/branding/ is a separate, crawlable page that can have its own title, description, canonical URL, and term description. Each category becomes a focused topic instead of a hidden filter state.

03 — Setup

Project category archive navigation setup

Create two templates, one shared partial, and make sure your taxonomy is registered for the projects post type.

wp-content/themes/your-theme/
│   ├── archive-projects.php
│   ├── taxonomy-project_cat.php
│   ├── template-parts/
│   │   └── project-archive-grid.php
│   ├── functions.php
    └── style.css
1

Create the template files

Create archive-projects.php, taxonomy-project_cat.php, and template-parts/project-archive-grid.php. If your CPT slug is singular, rename the archive file to archive-project.php.

2

Register or confirm the taxonomy

Your taxonomy must be attached to the projects post type. If it already exists, you do not need this code. If you are starting fresh, paste this in functions.php.

functions.php
php
<?php
// Paste the code below into your theme's functions.php.
// Do NOT copy the <?php line above - your functions.php already has one.

add_action( 'init', function() {

    register_taxonomy( 'project_cat', array( 'projects' ), array(
        'labels' => array(
            'name'          => 'Project Categories',
            'singular_name' => 'Project Category',
        ),
        'public'       => true,
        'hierarchical' => true,
        'show_in_rest' => true,
        'rewrite'      => array(
            'slug' => 'project-category',
        ),
    ) );

} );
3

Flush permalinks once

Go to Settings -> Permalinks and click Save Changes. This refreshes WordPress rewrite rules so the archive and taxonomy URLs stop returning 404s.

4

Add the CSS

Paste the CSS into your theme stylesheet, then adjust colors and spacing to match your design.

style.css
css
.project-filter {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    margin: 32px 0;
}

.project-filter__link {
    border: 1px solid #d8d8d8;
    border-radius: 999px;
    color: #333;
    padding: 8px 14px;
    text-decoration: none;
}

.project-filter__link.is-active {
    background: #111;
    border-color: #111;
    color: #fff;
}

.project-grid {
    display: grid;
    gap: 24px;
    grid-template-columns: repeat(4, minmax(0, 1fr));
}

.project-card__media {
    aspect-ratio: 4 / 3;
    background: #f3f3f3;
    display: block;
    overflow: hidden;
}

.project-card__media img {
    height: 100%;
    object-fit: cover;
    width: 100%;
}

@media (max-width: 1024px) {
    .project-grid {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

@media (max-width: 640px) {
    .project-grid {
        grid-template-columns: 1fr;
    }
}

Getting 404s after adding your template files? Go to Settings → Permalinks and click Save Changes. WordPress rebuilds its rewrite rules at that point — without it, new archive and taxonomy URLs return 404.

5

Wrong posts showing on the archive

Check that your CPT slug matches the template filename exactly. If the post type is registered as project, the archive file must be archive-project.php — not archive-projects.php. WordPress uses the exact slug to pick the template file.

6

Category filter links return 404

The taxonomy must be registered and attached to the correct post type before its term archive URLs work. If you registered the taxonomy after creating posts, go to Settings → Permalinks → Save Changes once more. Also confirm the taxonomy rewrite slug matches what you expect in the URL.

7

Active class not appearing on filter links

The active class relies on is_post_type_archive() and is_tax(). These only return true inside the correct archive template. If you are loading the navigation from a sidebar or widget outside the archive context, the conditionals will always be false. Keep the navigation inside the archive template, or pass the current archive state as a variable.

04 — Making it your own

Customizing your project category archive navigation

A few small changes make this fit most project archives.

1

Use a different CPT slug

Change $post_type = "projects"; to your real post type slug. Then rename the archive template to match. WordPress uses archive-{post_type}.php.

2

Show empty terms while building

During development, change hide_empty to false so categories appear even before projects are assigned. Switch it back to true before launch.

3

Control posts per page

Use WordPress Reading settings for the simplest setup, or use pre_get_posts if this archive needs a different number from the blog. For a 4 column grid, 8, 12, or 16 posts per page usually feels balanced.

Do not replace the main query with a new WP_Query unless you need to. A custom query can break pagination and confuse SEO plugins if it is not wired carefully. Archive templates already have the right query.

SEO tip — write a description for each category. Go to Projects → Project Categories in your WordPress admin. Click any category, fill in the Description field, and save. The template already prints this on the term archive page via term_description(), giving each category page unique content that search engines can index independently. Useful references: WordPress template hierarchy, get_terms(), project archive example, and more WordPress snippets.

0 comments

No comments yet. Be the first.

Leave a Reply

Project category archive navigation is the standard WordPress pattern for filtering a custom post type by taxonomy terms without JavaScript. Instead of toggling visibility on a single page, each category gets its own real archive URL that search engines can crawl independently and visitors can bookmark. This snippet builds a complete project category archive navigation system: an All link, individual project_cat term links, active state classes using WordPress conditional tags, and a responsive 4-column project grid.

Setting up project category archive navigation requires two template files. The archive-projects.php file handles the main projects archive. The taxonomy-project_cat.php file handles each individual term archive. Both files load one shared partial — template-parts/project-archive-grid.php — which keeps the category navigation, active class logic, grid markup, and pagination in one place. This shared partial approach means any future changes to the project category archive navigation only need to happen in one file.

The active class in the project category archive navigation relies on three WordPress conditional functions: is_post_type_archive(), is_tax(), and get_queried_object(). On the main project archive, the All link receives the is-active class. On a term archive, the matching term link becomes active. The snippet also adds aria-current="page" on the active link for screen reader accessibility and esc_url, esc_html escaping on every output for security.

The 4-column grid runs on the WordPress main query, not a custom WP_Query. On archive-projects.php, WordPress queries all published projects automatically. On taxonomy-project_cat.php, WordPress restricts the query to projects inside the selected term. Using the main query keeps pagination, SEO plugin titles, Rank Math canonical URLs, and term descriptions accurate without any extra configuration.

Using real taxonomy URLs instead of JavaScript filtering gives each project category its own page title, meta description, canonical URL, and term description. Search engines can crawl and index /project-category/branding/ independently from /project-category/web-design/, which improves topical depth across the projects section of the site. For related examples, see the projects archive and more WordPress PHP code snippets. For API references, see the WordPress template hierarchy and get_terms() documentation.

Chat on WhatsApp