fix: support slug and where[] query params in custom posts route

The custom /api/posts route intercepted all post queries but only
supported listing parameters (category, type, page). Frontend detail
pages sending where[slug][equals]=X got all posts back, always
showing the latest post regardless of which article was clicked.

Now parses slug from both ?slug=X and ?where[slug][equals]=X format.
Replaced getPostsByCategory with direct payload.find using properly
typed Where conditions. Detail queries (with slug) include content
and readingTime in the response.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martin Porwoll 2026-02-27 15:58:32 +00:00
parent 6d13361ad4
commit 9e791648e9

View file

@ -3,8 +3,8 @@
import { NextRequest, NextResponse } from 'next/server'
import { getPayload } from 'payload'
import type { Where } from 'payload'
import config from '@payload-config'
import { getPostsByCategory } from '@/lib/search'
import type { Category } from '@/payload-types'
import {
searchLimiter,
@ -42,11 +42,12 @@ export async function GET(request: NextRequest) {
)
}
// Parse query parameters
// Parse query parameters (supports both ?key=val and ?where[key][equals]=val formats)
const { searchParams } = new URL(request.url)
const category = searchParams.get('category')?.trim()
const type = searchParams.get('type')?.trim() as 'blog' | 'news' | 'press' | 'announcement' | undefined
const tenantParam = searchParams.get('tenant') || searchParams.get('where[tenant][equals]')
const slugParam = searchParams.get('slug')?.trim() || searchParams.get('where[slug][equals]')?.trim()
const pageParam = searchParams.get('page')
const limitParam = searchParams.get('limit')
const localeParam = searchParams.get('locale')?.trim()
@ -90,47 +91,82 @@ export async function GET(request: NextRequest) {
// Get payload instance
const payload = await getPayload({ config })
// Get posts
const result = await getPostsByCategory(payload, {
tenantId,
categorySlug: category,
type,
locale,
// Build query: slug lookup uses direct payload.find for full where support
const whereConditions: Where[] = [
{ tenant: { equals: tenantId } },
{ status: { equals: 'published' } },
]
if (slugParam) {
whereConditions.push({ slug: { equals: slugParam } })
}
if (type) {
whereConditions.push({ type: { equals: type } })
}
if (category) {
// Category filter by slug: look up category first
const catResult = await payload.find({
collection: 'categories',
where: {
slug: { equals: category },
...(tenantId ? { tenant: { equals: tenantId } } : {}),
},
locale: locale as 'de' | 'en',
limit: 1,
})
if (catResult.docs.length > 0) {
whereConditions.push({ categories: { contains: catResult.docs[0].id } })
}
}
const result = await payload.find({
collection: 'posts',
where: { and: whereConditions },
locale: locale as 'de' | 'en',
fallbackLocale: 'de',
page,
limit,
sort: '-publishedAt',
depth: slugParam ? 2 : 1,
})
// Transform response
// Transform response — slug lookup returns full post, listing returns summary
const response = {
docs: result.docs.map((post) => ({
id: post.id,
title: post.title,
slug: post.slug,
excerpt: post.excerpt || null,
publishedAt: post.publishedAt || null,
type: (post as typeof post & { type?: string }).type || 'blog',
featuredImage: post.featuredImage && typeof post.featuredImage === 'object'
? {
url: post.featuredImage.url,
alt: post.featuredImage.alt,
width: post.featuredImage.width,
height: post.featuredImage.height,
}
: null,
category: Array.isArray(post.categories) && post.categories.length > 0
? (() => {
const firstCat = post.categories.find(
(cat): cat is Category => cat !== null && typeof cat === 'object' && 'name' in cat
)
return firstCat ? { name: firstCat.name, slug: firstCat.slug } : null
})()
: null,
categories: Array.isArray(post.categories)
? post.categories
.filter((cat): cat is Category => cat !== null && typeof cat === 'object' && 'name' in cat)
.map((cat) => ({ name: cat.name, slug: cat.slug }))
: [],
})),
docs: result.docs.map((post) => {
const base = {
id: post.id,
title: post.title,
slug: post.slug,
excerpt: post.excerpt || null,
publishedAt: post.publishedAt || null,
type: (post as typeof post & { type?: string }).type || 'blog',
featuredImage: post.featuredImage && typeof post.featuredImage === 'object'
? {
url: post.featuredImage.url,
alt: post.featuredImage.alt,
width: post.featuredImage.width,
height: post.featuredImage.height,
}
: null,
category: Array.isArray(post.categories) && post.categories.length > 0
? (() => {
const firstCat = post.categories.find(
(cat): cat is Category => cat !== null && typeof cat === 'object' && 'name' in cat
)
return firstCat ? { name: firstCat.name, slug: firstCat.slug } : null
})()
: null,
categories: Array.isArray(post.categories)
? post.categories
.filter((cat): cat is Category => cat !== null && typeof cat === 'object' && 'name' in cat)
.map((cat) => ({ name: cat.name, slug: cat.slug }))
: [],
}
// Include content and extra fields for detail queries (slug lookup)
if (slugParam) {
return { ...base, content: post.content, readingTime: (post as typeof post & { readingTime?: number }).readingTime }
}
return base
}),
pagination: {
page: result.page,
limit,
@ -140,8 +176,8 @@ export async function GET(request: NextRequest) {
hasPrevPage: result.hasPrevPage,
},
filters: {
category,
type,
...(category ? { category } : {}),
...(type ? { type } : {}),
locale,
tenant: tenantId,
},