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>
);
}