Skip to main content
Version: App Router (1.5+)

queryTerms

The queryTerms function is used to fetch taxonomy terms (categories, tags, custom taxonomies) in Next.js App Router Server Components. This replaces the useTerms hook used in Pages Router.

Usage

Basic Categories List

app/categories/page.tsx
import { queryTerms } from '@headstartwp/next/app';
import type { Metadata } from 'next';
import Link from 'next/link';
import type { HeadstartWPRoute } from '@headstartwp/next/app';

async function query() {
return queryTerms({
routeParams: {},
params: {
taxonomy: 'category',
per_page: 50,
orderby: 'count',
order: 'desc',
},
options: {
next: {
revalidate: 3600, // Revalidate every hour
},
},
});
}

export async function generateMetadata(): Promise<Metadata> {
return {
title: 'Categories',
description: 'Browse all post categories',
};
}

export default async function CategoriesPage({ params }: HeadstartWPRoute) {
const { data } = await query();

return (
<main>
<h1>Categories</h1>

<div className="categories-grid">
{data.terms.map(category => (
<div key={category.id} className="category-card">
<h3>
<Link href={`/category/${category.slug}`}>
{category.name}
</Link>
</h3>

{category.description && (
<p>{category.description}</p>
)}

<div className="category-meta">
<span>{category.count} posts</span>
</div>
</div>
))}
</div>
</main>
);
}

Tags Cloud

app/tags/page.tsx
import { queryTerms } from '@headstartwp/next/app';
import type { HeadstartWPRoute } from '@headstartwp/next/app';

export default async function TagsPage({ params }: HeadstartWPRoute) {
const { data } = await queryTerms({
routeParams: {},
params: {
taxonomy: 'post_tag',
per_page: 100,
orderby: 'count',
order: 'desc',
},
});

return (
<main>
<h1>Tags</h1>

<div className="tags-cloud">
{data.terms.map(tag => (
<Link
key={tag.id}
href={`/tag/${tag.slug}`}
className={`tag tag-size-${Math.min(Math.floor(tag.count / 5) + 1, 5)}`}
style={{
fontSize: `${Math.min(tag.count * 0.1 + 1, 2)}rem`,
}}
>
{tag.name} ({tag.count})
</Link>
))}
</div>
</main>
);
}

Parameters

Function Signature

queryTerms({
routeParams,
params,
options?
}): Promise<{
data: {
terms: TermEntity[],
pageInfo: PageInfo
},
config: HeadlessConfig
}>

params

ParameterTypeDescription
taxonomystringTaxonomy name (category, post_tag, custom taxonomy)
per_pagenumberNumber of terms per page (default: 10)
pagenumberPage number for pagination
searchstringSearch terms by name
orderbystringOrder by (name, count, slug, etc.)
order'asc' | 'desc'Sort order
hide_emptybooleanHide terms with no posts (default: true)
parentnumberGet child terms of specific parent
includenumber[]Include specific term IDs
excludenumber[]Exclude specific term IDs

Return Value

data.terms

Array of term objects containing:

  • id - Term ID
  • name - Term name
  • slug - Term slug
  • description - Term description
  • link - Term archive URL
  • count - Number of posts in this term
  • parent - Parent term ID (for hierarchical taxonomies)
  • taxonomy - Taxonomy name

data.pageInfo

Pagination information for the terms.

Advanced Examples

Hierarchical Categories

app/categories/page.tsx
import { queryTerms } from '@headstartwp/next/app';
import type { HeadstartWPRoute } from '@headstartwp/next/app';

async function getCategoriesHierarchy() {
// Get all categories
const { data } = await queryTerms({
routeParams: {},
params: {
taxonomy: 'category',
per_page: 100,
orderby: 'name',
parent: 0, // Only top-level categories
},
});

// Get children for each category
const categoriesWithChildren = await Promise.all(
data.terms.map(async (category) => {
const { data: childrenData } = await queryTerms({
routeParams: {},
params: {
taxonomy: 'category',
parent: category.id,
per_page: 50,
},
});

return {
...category,
children: childrenData.terms,
};
})
);

return categoriesWithChildren;
}

export default async function HierarchicalCategoriesPage() {
const categories = await getCategoriesHierarchy();

return (
<main>
<h1>Categories</h1>

<div className="categories-hierarchy">
{categories.map(category => (
<div key={category.id} className="category-group">
<h2>
<Link href={`/category/${category.slug}`}>
{category.name} ({category.count})
</Link>
</h2>

{category.children.length > 0 && (
<ul className="subcategories">
{category.children.map(child => (
<li key={child.id}>
<Link href={`/category/${child.slug}`}>
{child.name} ({child.count})
</Link>
</li>
))}
</ul>
)}
</div>
))}
</div>
</main>
);
}

Terms with Pagination

app/categories/page/[page]/page.tsx
import { queryTerms } from '@headstartwp/next/app';

export async function generateStaticParams() {
// Get total number of categories
const { data } = await queryTerms({
routeParams: {},
params: {
taxonomy: 'category',
per_page: 1, // Just to get total count
},
});

const totalPages = data.pageInfo.totalPages;
return Array.from({ length: totalPages }, (_, i) => ({
page: (i + 1).toString(),
}));
}

export default async function CategoriesPaginationPage({ params }) {
const resolvedParams = await params;
const page = parseInt(resolvedParams.page);

const { data, pageInfo } = await queryTerms({
routeParams: await params,
params: {
taxonomy: 'category',
per_page: 20,
page,
},
});

return (
<main>
<h1>Categories - Page {page}</h1>

<div className="categories-grid">
{data.terms.map(category => (
<CategoryCard key={category.id} category={category} />
))}
</div>

{/* Pagination */}
<div className="pagination">
{pageInfo.hasPreviousPage && (
<Link href={`/categories/page/${page - 1}`}>
Previous
</Link>
)}
{pageInfo.hasNextPage && (
<Link href={`/categories/page/${page + 1}`}>
Next
</Link>
)}
</div>
</main>
);
}