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 { NextRequest, NextResponse } from 'next/server'
import { getPayload } from 'payload' import { getPayload } from 'payload'
import type { Where } from 'payload'
import config from '@payload-config' import config from '@payload-config'
import { getPostsByCategory } from '@/lib/search'
import type { Category } from '@/payload-types' import type { Category } from '@/payload-types'
import { import {
searchLimiter, 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 { searchParams } = new URL(request.url)
const category = searchParams.get('category')?.trim() const category = searchParams.get('category')?.trim()
const type = searchParams.get('type')?.trim() as 'blog' | 'news' | 'press' | 'announcement' | undefined const type = searchParams.get('type')?.trim() as 'blog' | 'news' | 'press' | 'announcement' | undefined
const tenantParam = searchParams.get('tenant') || searchParams.get('where[tenant][equals]') 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 pageParam = searchParams.get('page')
const limitParam = searchParams.get('limit') const limitParam = searchParams.get('limit')
const localeParam = searchParams.get('locale')?.trim() const localeParam = searchParams.get('locale')?.trim()
@ -90,19 +91,48 @@ export async function GET(request: NextRequest) {
// Get payload instance // Get payload instance
const payload = await getPayload({ config }) const payload = await getPayload({ config })
// Get posts // Build query: slug lookup uses direct payload.find for full where support
const result = await getPostsByCategory(payload, { const whereConditions: Where[] = [
tenantId, { tenant: { equals: tenantId } },
categorySlug: category, { status: { equals: 'published' } },
type, ]
locale, 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, page,
limit, limit,
sort: '-publishedAt',
depth: slugParam ? 2 : 1,
}) })
// Transform response // Transform response — slug lookup returns full post, listing returns summary
const response = { const response = {
docs: result.docs.map((post) => ({ docs: result.docs.map((post) => {
const base = {
id: post.id, id: post.id,
title: post.title, title: post.title,
slug: post.slug, slug: post.slug,
@ -130,7 +160,13 @@ export async function GET(request: NextRequest) {
.filter((cat): cat is Category => cat !== null && typeof cat === 'object' && 'name' in cat) .filter((cat): cat is Category => cat !== null && typeof cat === 'object' && 'name' in cat)
.map((cat) => ({ name: cat.name, slug: cat.slug })) .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: { pagination: {
page: result.page, page: result.page,
limit, limit,
@ -140,8 +176,8 @@ export async function GET(request: NextRequest) {
hasPrevPage: result.hasPrevPage, hasPrevPage: result.hasPrevPage,
}, },
filters: { filters: {
category, ...(category ? { category } : {}),
type, ...(type ? { type } : {}),
locale, locale,
tenant: tenantId, tenant: tenantId,
}, },