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

queryPosts

The queryPosts function is used to fetch multiple posts, pages, or custom post types in Next.js App Router Server Components. It's the async/await equivalent of the usePosts hook used in Pages Router.

Usage​

Basic Example​

app/page.tsx
import { queryPosts } from '@headstartwp/next/app';
import Link from 'next/link';
import type { HeadstartWPRoute } from '@headstartwp/next/app';
import { SafeHtml } from '@headstartwp/core/react';

export default async function HomePage({ params }: HeadstartWPRoute) {
const { data } = await queryPosts({
routeParams: await params,
params: {
postType: 'post',
per_page: 4,
},
});

return (
<main>
<h1>Welcome to Our Site</h1>

<section>
<h2>Latest Posts</h2>
<div className="posts-grid">
{data.posts.map(post => (
<article key={post.id}>
<h3>
<Link href={post.link}>
{post.title.rendered}
</Link>
</h3>
<SafeHtml html={post.excerpt.rendered} />
</article>
))}
</div>
</section>
</main>
);
}

Dynamic Archive Routes​

For catch-all routes like [...path], HeadstartWP automatically extracts the category, page, or other archive parameters from the URL structure and applies the appropriate filters.

app/[...path]/page.tsx
import { queryPosts } from '@headstartwp/next/app';
import type { HeadstartWPRoute } from '@headstartwp/next/app';
import { SafeHtml } from '@headstartwp/core/react';

export default async function CategoryArchivePage({ params }: HeadstartWPRoute) {
const { data } = await queryPosts({
routeParams: await params,
params: {
postType: 'post',
per_page: 12,
},
});

return (
<main>
<h1>{data.queriedObject?.name || 'Category Archive'}</h1>

{data.posts.map(post => (
<article key={post.id}>
<h2>{post.title.rendered}</h2>
<SafeHtml html={post.excerpt.rendered} />
</article>
))}

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

Parameters​

Function Signature​

queryPosts({
routeParams,
params,
options?
}): Promise<{
data: {
posts: PostEntity[],
queriedObject?: QueriedObject,
pageInfo: PageInfo
},
config: HeadlessConfig
}>

routeParams​

The route parameters from Next.js App Router. Must be awaited since Next.js 15+.

const { data } = await queryPosts({
routeParams: await params, // Always await params
// ...
});

params​

The query parameters for fetching posts:

ParameterTypeDescription
postTypestring | string[]Post type(s) to fetch
per_pagenumberNumber of posts per page (default: 10)
pagenumberPage number for pagination
categorystring | numberCategory slug or ID
tagstring | numberTag slug or ID
authorstring | numberAuthor slug or ID
searchstringSearch term
orderbystringOrder posts by (date, title, etc.)
order'asc' | 'desc'Sort order
meta_queryobjectCustom field queries
tax_queryobjectTaxonomy queries

options​

Next.js App Router specific options:

const { data } = await queryPosts({
routeParams: await params,
params: { postType: 'post' },
options: {
next: {
revalidate: 600, // Revalidate every 10 minutes
tags: ['posts', 'blog'], // Cache tags
},
cache: 'force-cache', // Cache strategy
},
});

Return Value​

data.posts​

Array of post objects.

data.queriedObject​

When fetching posts by category, tag, or author, contains information about the queried term or author.

data.pageInfo​

Pagination information:

interface PageInfo {
page: number;
totalPages: number;
totalItems: number;
hasNextPage: boolean;
hasPreviousPage: boolean;
}

Advanced Examples​

Custom Post Type Archive​

To use custom post types with queryPosts, you must first register them in your headstartwp.config.js file. See the custom post types configuration for detailed setup instructions.

app/products/page.tsx
import { queryPosts } from '@headstartwp/next/app';
import type { HeadstartWPRoute } from '@headstartwp/next/app';
import { SafeHtml } from '@headstartwp/core/react';

export default async function ProductsPage({ params }: HeadstartWPRoute) {
const { data } = await queryPosts({
routeParams: await params,
params: {
postType: 'product',
per_page: 12,
orderby: 'menu_order',
order: 'asc',
meta_query: [
{
key: 'featured',
value: 'yes',
compare: '=',
},
],
},
});

return (
<main>
<h1>Featured Products</h1>

<div className="products-grid">
{data.posts.map(product => (
<div key={product.id} className="product-card">
<h3>{product.title.rendered}</h3>
<SafeHtml html={product.excerpt.rendered} />
</div>
))}
</div>
</main>
);
}

Static Generation​

Generate Static Params for Pagination​

app/blog/[...path]/page.tsx
import { queryPosts } from '@headstartwp/next/app';
import type { HeadstartWPRoute } from '@headstartwp/next/app';
import { SafeHtml } from '@headstartwp/core/react';

export async function generateStaticParams() {
// Get total number of posts to calculate pages
const { data } = await queryPosts({
routeParams: {},
params: {
postType: 'post',
per_page: 1, // Just to get total count
},
});

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

return pages;
}

export default async function BlogPaginationPage({ params }: HeadstartWPRoute) {
const { data } = await queryPosts({
routeParams: await params,
params: {
postType: 'post',
per_page: 10,
},
});

return (
<main>
<h1>Blog Posts - Page {data.pageInfo.page}</h1>
{/* Render posts */}
</main>
);
}

Caching and Performance​

Cache Tags for Revalidation​

const { data } = await queryPosts({
routeParams: await params,
params: { postType: 'post' },
options: {
next: {
tags: ['posts', 'blog-archive'],
},
},
});

// Revalidate from API route
// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';

export async function POST() {
revalidateTag('posts');
return Response.json({ revalidated: true });
}

Streaming with Suspense​

app/blog/page.tsx
import { Suspense } from 'react';
import { queryPosts } from '@headstartwp/next/app';
import { SafeHtml } from '@headstartwp/core/react';

async function PostsList() {
const { data } = await queryPosts({
routeParams: {},
params: { postType: 'post' },
});

return (
<div>
{data.posts.map(post => (
<article key={post.id}>
<h2>{post.title.rendered}</h2>
<SafeHtml html={post.excerpt.rendered} />
</article>
))}
</div>
);
}

export default function BlogPage() {
return (
<main>
<h1>Blog</h1>
<Suspense fallback={<div>Loading posts...</div>}>
<PostsList />
</Suspense>
</main>
);
}