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

queryPostOrPosts

The queryPostOrPosts function allows you to handle both single posts and archive pages in a single query. This is useful for catch-all routes that need to dynamically handle different content types. The function automatically determines whether the current route should return a single post or an archive of posts.

Basic Usage

Dynamic Route Handler

app/[...path]/page.tsx
import { queryPostOrPosts } from '@headstartwp/next/app';
import { notFound } from 'next/navigation';
import type { Metadata } from 'next';
import Link from 'next/link';
import type { HeadstartWPRoute } from '@headstartwp/next/app';
import { SafeHtml } from '@headstartwp/core/react';

export async function generateMetadata({ params }: HeadstartWPRoute): Promise<Metadata> {
const { seo } = await queryPostOrPosts({
routeParams: await params,
params: {
single: {
postType: ['post', 'page'],
},
archive: {
postType: 'post',
},
},
});

return seo.metadata;
}

export default async function DynamicPage({ params }: HeadstartWPRoute) {
const { isSingle, isArchive, data } = await queryPostOrPosts({
routeParams: await params,
params: {
single: {
postType: ['post', 'page'],
},
archive: {
postType: 'post',
},
},
});

if (isSingle && data.post) {
return (
<article>
<h1>{data.post.title.rendered}</h1>
<SafeHtml html={data.post.content.rendered} />
</article>
);
}

if (isArchive && data.posts) {
return (
<div className="posts-grid">
<h1>Blog Posts</h1>
{data.posts.map((post) => (
<article key={post.id}>
<h2>
<Link href={post.link}>{post.title.rendered}</Link>
</h2>
<SafeHtml html={post.excerpt.rendered} />
</article>
))}
</div>
);
}

// This shouldn't happen, but handle gracefully
notFound();
}

Parameters

The queryPostOrPosts function accepts the following parameters:

Function Signature

queryPostOrPosts<T extends PostEntity = PostEntity, P extends PostOrPostsParams = PostOrPostsParams>(
q: PostOrPostsQueryProps<T, P> = {},
_config?: HeadlessConfig
): Promise<QueryResult>

Query Parameters

interface PostOrPostsQueryProps {
routeParams: RouteParams;
params: {
single: PostParams; // Parameters for single post queries
archive: PostsParams; // Parameters for archive queries
priority?: 'single' | 'archive'; // Which to try first
routeMatchStrategy?: 'single' | 'archive'; // Route matching strategy
};
options?: {
next?: NextFetchRequestConfig;
cache?: RequestCache;
};
fetchStrategy?: PostOrPostsFetchStrategy;
}

Parameters Details

ParameterTypeDescription
params.singlePostParamsParameters used when querying for a single post
params.archivePostsParamsParameters used when querying for posts archive
params.priority'single' | 'archive'Which query type to try first (default: 'single')
params.routeMatchStrategy'single' | 'archive'Strategy for route matching
optionsQueryOptionsNext.js caching and fetch options

Return Value

The function returns a promise that resolves to:

interface QueryResult {
isSingle: boolean; // True if result is a single post
isArchive: boolean; // True if result is an archive
data: {
post?: PostEntity; // Available when isSingle is true
posts?: PostEntity[]; // Available when isArchive is true
queriedObject?: any; // Archive metadata (category, author, etc.)
pageInfo?: PageInfo; // Pagination info for archives
};
seo: {
metadata: Metadata; // Next.js metadata object
schema?: any; // JSON-LD schema data
};
config: HeadlessConfig; // Framework configuration
}

Advanced Usage

Custom Post Types

app/products/[[...path]]/page.tsx
import { queryPostOrPosts } from '@headstartwp/next/app';
import type { HeadstartWPRoute } from '@headstartwp/next/app';

export default async function ProductsPage({ params }: HeadstartWPRoute) {
const { isSingle, isArchive, data } = await queryPostOrPosts({
routeParams: await params,
params: {
single: {
postType: 'product',
},
archive: {
postType: 'product',
perPage: 12,
// Only show published products in archive
_fields: ['id', 'title', 'excerpt', 'link', 'featured_media'],
},
},
});

if (isSingle && data.post) {
// Single product page
return (
<div className="product-details">
<h1>{data.post.title.rendered}</h1>
<SafeHtml html={data.post.content.rendered} />

{/* Product-specific content */}
{data.post.acf && (
<div className="product-meta">
<p>Price: ${data.post.acf.price}</p>
<p>SKU: {data.post.acf.sku}</p>
</div>
)}
</div>
);
}

if (isArchive && data.posts) {
// Products archive
return (
<div className="products-grid">
<h1>Our Products</h1>
<div className="grid grid-cols-3 gap-6">
{data.posts.map(product => (
<div key={product.id} className="product-card">
<h3>
<Link href={product.link}>
{product.title.rendered}
</Link>
</h3>
<SafeHtml html={product.excerpt.rendered} />
</div>
))}
</div>
</div>
);
}
}

Priority and Route Matching

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

export default async function BlogPage({ params }: HeadstartWPRoute) {
const { isSingle, isArchive, data } = await queryPostOrPosts({
routeParams: await params,
params: {
single: {
postType: 'post',
},
archive: {
postType: 'post',
perPage: 10,
},
// Try archive first for this route
priority: 'archive',
routeMatchStrategy: 'archive',
},
});

// Component logic...
}

With Taxonomy Filtering

app/category/[[...path]]/page.tsx
import type { HeadstartWPRoute } from '@headstartwp/next/app';

export default async function CategoryPage({ params }: HeadstartWPRoute) {
const { isArchive, data } = await queryPostOrPosts({
routeParams: await params,
params: {
single: {
postType: 'post',
},
archive: {
postType: 'post',
taxonomy: 'category',
perPage: 10,
},
// For category routes, prioritize archive
priority: 'archive',
},
});

if (isArchive && data.posts) {
const category = data.queriedObject?.term;

return (
<main>
<h1>Category: {category?.name}</h1>
{category?.description && (
<p className="category-description">{category.description}</p>
)}

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

return <div>No posts found in this category.</div>;
}

Static Generation

Generate Static Params

app/[...path]/page.tsx
export async function generateStaticParams() {
try {
// Generate paths for both single posts and archives
const { data } = await queryPostOrPosts({
routeParams: {},
params: {
single: { postType: ['post', 'page'] },
archive: { postType: 'post', perPage: 100 },
},
});

const paths: Array<{ path: string[] }> = [];

// Add single post/page paths
if (data.posts) {
data.posts.forEach(post => {
const url = new URL(post.link);
const pathSegments = url.pathname.split('/').filter(Boolean);
paths.push({ path: pathSegments });
});
}

return paths;
} catch (error) {
console.error('Failed to generate static params:', error);
return [];
}
}

Performance Optimization

Caching Configuration

const result = await queryPostOrPosts({
routeParams: await params,
params: {
single: { postType: ['post', 'page'] },
archive: { postType: 'post' },
},
options: {
next: {
revalidate: 3600, // Revalidate every hour
tags: ['posts', 'pages'], // Cache tags for on-demand revalidation
},
cache: 'force-cache',
},
});

Field Selection

const result = await queryPostOrPosts({
routeParams: await params,
params: {
single: {
postType: ['post', 'page'],
// Full content for single posts
},
archive: {
postType: 'post',
// Minimal fields for archive performance
_fields: ['id', 'title', 'excerpt', 'link', 'date'],
},
},
});

TypeScript Support

Generic Types

import type { PostEntity } from '@headstartwp/core';

interface CustomPostEntity extends PostEntity {
acf: {
price: number;
sku: string;
};
}

const result = await queryPostOrPosts<CustomPostEntity>({
routeParams: await params,
params: {
single: { postType: 'product' },
archive: { postType: 'product' },
},
});

// TypeScript knows the shape of result.data.post and result.data.posts
if (result.isSingle && result.data.post) {
console.log(result.data.post.acf.price); // Type-safe access
}

Custom Parameters

import type { PostOrPostsParams } from '@headstartwp/core';

interface CustomParams extends PostOrPostsParams {
customField: string;
}

const result = await queryPostOrPosts<PostEntity, CustomParams>({
routeParams: await params,
params: {
single: {
postType: 'post',
customField: 'value',
},
archive: {
postType: 'post',
customField: 'value',
},
},
});

The queryPostOrPosts function provides a powerful, unified approach to handling both single and archive content in your App Router applications, with built-in TypeScript support, error handling, and performance optimizations.