From d57700d322a52f375916f41a920be5b248b93cd2 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Tue, 24 Feb 2026 19:45:33 +0100 Subject: [PATCH] fix: build stability (added try-catch to payload queries and removed generateStaticParams from generic pages) --- app/[locale]/[slug]/page.tsx | 14 -- lib/blog.ts | 126 ++++++++++-------- lib/mdx.ts | 247 +++++++++++++++++++---------------- lib/pages.ts | 163 ++++++++++++----------- next-env.d.ts | 2 +- package.json | 2 +- 6 files changed, 292 insertions(+), 262 deletions(-) diff --git a/app/[locale]/[slug]/page.tsx b/app/[locale]/[slug]/page.tsx index 5ec2bffe..bfa2e5f6 100644 --- a/app/[locale]/[slug]/page.tsx +++ b/app/[locale]/[slug]/page.tsx @@ -14,20 +14,6 @@ interface PageProps { }>; } -export async function generateStaticParams() { - const locales = ['en', 'de']; - const params = []; - - for (const locale of locales) { - const pages = await getAllPages(locale); - for (const page of pages) { - params.push({ locale, slug: page.slug }); - } - } - - return params; -} - export async function generateMetadata({ params }: PageProps): Promise { const { locale, slug } = await params; const pageData = await getPageBySlug(slug, locale); diff --git a/lib/blog.ts b/lib/blog.ts index 45c35e4c..dc441f84 100644 --- a/lib/blog.ts +++ b/lib/blog.ts @@ -57,64 +57,23 @@ export function isPostVisible(post: { frontmatter: { date: string; public?: bool } export async function getPostBySlug(slug: string, locale: string): Promise { - const payload = await getPayload({ config: configPromise }); + try { + const payload = await getPayload({ config: configPromise }); - const { docs } = await payload.find({ - collection: 'posts', - where: { - slug: { equals: slug }, - locale: { equals: locale }, - }, - draft: process.env.NODE_ENV === 'development', - limit: 1, - }); - - if (!docs || docs.length === 0) return null; - - const doc = docs[0]; - - return { - slug: doc.slug, - frontmatter: { - title: doc.title, - date: doc.date, - excerpt: doc.excerpt || '', - category: doc.category || '', - locale: doc.locale, - featuredImage: - typeof doc.featuredImage === 'object' && doc.featuredImage !== null - ? doc.featuredImage.sizes?.card?.url || doc.featuredImage.url - : null, - focalX: - typeof doc.featuredImage === 'object' && doc.featuredImage !== null - ? doc.featuredImage.focalX - : 50, - focalY: - typeof doc.featuredImage === 'object' && doc.featuredImage !== null - ? doc.featuredImage.focalY - : 50, - public: doc._status === 'published', - } as PostFrontmatter, - content: doc.content as any, // Native Lexical Editor State - }; -} - -export async function getAllPosts(locale: string): Promise { - const payload = await getPayload({ config: configPromise }); - // Query only published posts (access checks applied automatically by Payload!) - const { docs } = await payload.find({ - collection: 'posts', - where: { - locale: { - equals: locale, + const { docs } = await payload.find({ + collection: 'posts', + where: { + slug: { equals: slug }, + locale: { equals: locale }, }, - }, - sort: '-date', - draft: process.env.NODE_ENV === 'development', // Includes Drafts if running locally - limit: 100, - }); + draft: process.env.NODE_ENV === 'development', + limit: 1, + }); + + if (!docs || docs.length === 0) return null; + + const doc = docs[0]; - return docs.map((doc) => { return { slug: doc.slug, frontmatter: { @@ -135,11 +94,62 @@ export async function getAllPosts(locale: string): Promise { typeof doc.featuredImage === 'object' && doc.featuredImage !== null ? doc.featuredImage.focalY : 50, + public: doc._status === 'published', } as PostFrontmatter, - // Pass the Lexical content object rather than raw markdown string - content: doc.content as any, + content: doc.content as any, // Native Lexical Editor State }; - }); + } catch (error) { + console.error(`[Payload] getPostBySlug failed for ${slug}:`, error); + return null; + } +} + +export async function getAllPosts(locale: string): Promise { + try { + const payload = await getPayload({ config: configPromise }); + // Query only published posts (access checks applied automatically by Payload!) + const { docs } = await payload.find({ + collection: 'posts', + where: { + locale: { + equals: locale, + }, + }, + sort: '-date', + draft: process.env.NODE_ENV === 'development', // Includes Drafts if running locally + limit: 100, + }); + + return docs.map((doc) => { + return { + slug: doc.slug, + frontmatter: { + title: doc.title, + date: doc.date, + excerpt: doc.excerpt || '', + category: doc.category || '', + locale: doc.locale, + featuredImage: + typeof doc.featuredImage === 'object' && doc.featuredImage !== null + ? doc.featuredImage.sizes?.card?.url || doc.featuredImage.url + : null, + focalX: + typeof doc.featuredImage === 'object' && doc.featuredImage !== null + ? doc.featuredImage.focalX + : 50, + focalY: + typeof doc.featuredImage === 'object' && doc.featuredImage !== null + ? doc.featuredImage.focalY + : 50, + } as PostFrontmatter, + // Pass the Lexical content object rather than raw markdown string + content: doc.content as any, + }; + }); + } catch (error) { + console.error(`[Payload] getAllPosts failed for ${locale}:`, error); + return []; + } } export async function getAllPostsMetadata(locale: string): Promise[]> { diff --git a/lib/mdx.ts b/lib/mdx.ts index b50bdb6f..d67cf022 100644 --- a/lib/mdx.ts +++ b/lib/mdx.ts @@ -79,135 +79,114 @@ export async function getProductMetadata( } export async function getProductBySlug(slug: string, locale: string): Promise { - const payload = await getPayload({ config: configPromise }); - const fileSlug = await mapSlugToFileSlug(slug, locale); + try { + const payload = await getPayload({ config: configPromise }); + const fileSlug = await mapSlugToFileSlug(slug, locale); - let result = await payload.find({ - collection: 'products', - where: { - and: [{ slug: { equals: fileSlug } }, { locale: { equals: locale } }], - }, - depth: 1, // Auto-resolve Media logic - limit: 1, - }); - - let isFallback = false; - - if (result.docs.length === 0 && locale !== 'en') { - // Fallback to English - result = await payload.find({ + let result = await payload.find({ collection: 'products', where: { - and: [{ slug: { equals: fileSlug } }, { locale: { equals: 'en' } }], + and: [{ slug: { equals: fileSlug } }, { locale: { equals: locale } }], }, - depth: 1, + depth: 1, // Auto-resolve Media logic limit: 1, }); - if (result.docs.length > 0) { - isFallback = true; + + let isFallback = false; + + if (result.docs.length === 0 && locale !== 'en') { + // Fallback to English + result = await payload.find({ + collection: 'products', + where: { + and: [{ slug: { equals: fileSlug } }, { locale: { equals: 'en' } }], + }, + depth: 1, + limit: 1, + }); + if (result.docs.length > 0) { + isFallback = true; + } } - } - if (result.docs.length > 0) { - const doc = result.docs[0]; + if (result.docs.length > 0) { + const doc = result.docs[0]; - // Map Images correctly from resolved Media docs - const resolvedImages = ((doc.images as any[]) || []) - .map((img) => (typeof img === 'string' ? img : img.url)) - .filter(Boolean); - - if (resolvedImages.length === 0) return null; - - return { - slug: doc.slug, - frontmatter: { - title: doc.title, - sku: doc.sku, - description: doc.description, - categories: Array.isArray(doc.categories) ? doc.categories.map((c: any) => c.category) : [], - images: resolvedImages, - locale: doc.locale, - isFallback, - }, - content: doc.content, // Lexical payload instead of raw MDX String - }; - } - - return null; -} - -export async function getAllProductSlugs(locale: string): Promise { - const payload = await getPayload({ config: configPromise }); - const result = await payload.find({ - collection: 'products', - where: { - locale: { - equals: locale, - }, - }, - pagination: false, // get all docs - }); - - return result.docs.map((doc) => doc.slug); -} - -export async function getAllProducts(locale: string): Promise { - const payload = await getPayload({ config: configPromise }); - - const selectFields = { - title: true, - slug: true, - sku: true, - description: true, - categories: true, - images: true, - locale: true, - } as const; - - // Get products for this locale - const result = await payload.find({ - collection: 'products', - where: { locale: { equals: locale } }, - depth: 1, - pagination: false, - select: selectFields, - }); - - let products: ProductMdx[] = result.docs - .filter((doc) => { + // Map Images correctly from resolved Media docs const resolvedImages = ((doc.images as any[]) || []) .map((img) => (typeof img === 'string' ? img : img.url)) .filter(Boolean); - return resolvedImages.length > 0; - }) - .map((doc) => ({ - slug: doc.slug, - frontmatter: { - title: doc.title, - sku: doc.sku || '', - description: doc.description || '', - categories: Array.isArray(doc.categories) ? doc.categories.map((c: any) => c.category) : [], - images: ((doc.images as any[]) || []) - .map((img) => (typeof img === 'string' ? img : img.url)) - .filter(Boolean), - locale: doc.locale, - }, - content: null, - })); - // Also include English fallbacks for slugs not in this locale - if (locale !== 'en') { - const localeSlugs = new Set(products.map((p) => p.slug)); - const enResult = await payload.find({ + if (resolvedImages.length === 0) return null; + + return { + slug: doc.slug, + frontmatter: { + title: doc.title, + sku: doc.sku, + description: doc.description, + categories: Array.isArray(doc.categories) + ? doc.categories.map((c: any) => c.category) + : [], + images: resolvedImages, + locale: doc.locale, + isFallback, + }, + content: doc.content, // Lexical payload instead of raw MDX String + }; + } + + return null; + } catch (error) { + console.error(`[Payload] getProductBySlug failed for ${slug}:`, error); + return null; + } +} + +export async function getAllProductSlugs(locale: string): Promise { + try { + const payload = await getPayload({ config: configPromise }); + const result = await payload.find({ collection: 'products', - where: { locale: { equals: 'en' } }, + where: { + locale: { + equals: locale, + }, + }, + pagination: false, // get all docs + }); + + return result.docs.map((doc) => doc.slug); + } catch (error) { + console.error(`[Payload] getAllProductSlugs failed for ${locale}:`, error); + return []; + } +} + +export async function getAllProducts(locale: string): Promise { + try { + const payload = await getPayload({ config: configPromise }); + + const selectFields = { + title: true, + slug: true, + sku: true, + description: true, + categories: true, + images: true, + locale: true, + } as const; + + // Get products for this locale + const result = await payload.find({ + collection: 'products', + where: { locale: { equals: locale } }, depth: 1, pagination: false, select: selectFields, }); - const fallbacks = enResult.docs - .filter((doc) => !localeSlugs.has(doc.slug)) + let products: ProductMdx[] = result.docs .filter((doc) => { const resolvedImages = ((doc.images as any[]) || []) .map((img) => (typeof img === 'string' ? img : img.url)) @@ -227,15 +206,55 @@ export async function getAllProducts(locale: string): Promise { .map((img) => (typeof img === 'string' ? img : img.url)) .filter(Boolean), locale: doc.locale, - isFallback: true, }, content: null, })); - products = [...products, ...fallbacks]; - } + // Also include English fallbacks for slugs not in this locale + if (locale !== 'en') { + const localeSlugs = new Set(products.map((p) => p.slug)); + const enResult = await payload.find({ + collection: 'products', + where: { locale: { equals: 'en' } }, + depth: 1, + pagination: false, + select: selectFields, + }); - return products; + const fallbacks = enResult.docs + .filter((doc) => !localeSlugs.has(doc.slug)) + .filter((doc) => { + const resolvedImages = ((doc.images as any[]) || []) + .map((img) => (typeof img === 'string' ? img : img.url)) + .filter(Boolean); + return resolvedImages.length > 0; + }) + .map((doc) => ({ + slug: doc.slug, + frontmatter: { + title: doc.title, + sku: doc.sku || '', + description: doc.description || '', + categories: Array.isArray(doc.categories) + ? doc.categories.map((c: any) => c.category) + : [], + images: ((doc.images as any[]) || []) + .map((img) => (typeof img === 'string' ? img : img.url)) + .filter(Boolean), + locale: doc.locale, + isFallback: true, + }, + content: null, + })); + + products = [...products, ...fallbacks]; + } + + return products; + } catch (error) { + console.error(`[Payload] getAllProducts failed for ${locale}:`, error); + return []; + } } export async function getAllProductsMetadata(locale: string): Promise[]> { diff --git a/lib/pages.ts b/lib/pages.ts index ebe9487e..5adc9099 100644 --- a/lib/pages.ts +++ b/lib/pages.ts @@ -16,97 +16,112 @@ export interface PageMdx { } export async function getPageBySlug(slug: string, locale: string): Promise { - const payload = await getPayload({ config: configPromise }); + try { + const payload = await getPayload({ config: configPromise }); - const result = await payload.find({ - collection: 'pages' as any, - where: { - slug: { equals: slug }, - locale: { equals: locale }, - }, - limit: 1, - }); + const result = await payload.find({ + collection: 'pages' as any, + where: { + slug: { equals: slug }, + locale: { equals: locale }, + }, + limit: 1, + }); - const docs = result.docs as any[]; + const docs = result.docs as any[]; - if (!docs || docs.length === 0) return null; + if (!docs || docs.length === 0) return null; - const doc = docs[0]; + const doc = docs[0]; - return { - slug: doc.slug, - frontmatter: { - title: doc.title, - excerpt: doc.excerpt || '', - locale: doc.locale, - featuredImage: - typeof doc.featuredImage === 'object' && doc.featuredImage !== null - ? doc.featuredImage.sizes?.card?.url || doc.featuredImage.url - : null, - } as PageFrontmatter, - content: doc.content as any, // Native Lexical Editor State - }; + return { + slug: doc.slug, + frontmatter: { + title: doc.title, + excerpt: doc.excerpt || '', + locale: doc.locale, + featuredImage: + typeof doc.featuredImage === 'object' && doc.featuredImage !== null + ? doc.featuredImage.sizes?.card?.url || doc.featuredImage.url + : null, + } as PageFrontmatter, + content: doc.content as any, // Native Lexical Editor State + }; + } catch (error) { + console.error(`[Payload] getPageBySlug failed for ${slug}:`, error); + return null; + } } export async function getAllPages(locale: string): Promise { - const payload = await getPayload({ config: configPromise }); + try { + const payload = await getPayload({ config: configPromise }); - const result = await payload.find({ - collection: 'pages' as any, - where: { - locale: { - equals: locale, + const result = await payload.find({ + collection: 'pages' as any, + where: { + locale: { + equals: locale, + }, }, - }, - limit: 100, - }); + limit: 100, + }); - const docs = result.docs as any[]; + const docs = result.docs as any[]; - return docs.map((doc: any) => { - return { - slug: doc.slug, - frontmatter: { - title: doc.title, - excerpt: doc.excerpt || '', - locale: doc.locale, - featuredImage: - typeof doc.featuredImage === 'object' && doc.featuredImage !== null - ? doc.featuredImage.sizes?.card?.url || doc.featuredImage.url - : null, - } as PageFrontmatter, - content: doc.content as any, - }; - }); + return docs.map((doc: any) => { + return { + slug: doc.slug, + frontmatter: { + title: doc.title, + excerpt: doc.excerpt || '', + locale: doc.locale, + featuredImage: + typeof doc.featuredImage === 'object' && doc.featuredImage !== null + ? doc.featuredImage.sizes?.card?.url || doc.featuredImage.url + : null, + } as PageFrontmatter, + content: doc.content as any, + }; + }); + } catch (error) { + console.error(`[Payload] getAllPages failed for ${locale}:`, error); + return []; + } } export async function getAllPagesMetadata(locale: string): Promise[]> { - const payload = await getPayload({ config: configPromise }); + try { + const payload = await getPayload({ config: configPromise }); - const result = await payload.find({ - collection: 'pages' as any, - where: { - locale: { - equals: locale, + const result = await payload.find({ + collection: 'pages' as any, + where: { + locale: { + equals: locale, + }, }, - }, - limit: 100, - }); + limit: 100, + }); - const docs = result.docs as any[]; + const docs = result.docs as any[]; - return docs.map((doc: any) => { - return { - slug: doc.slug, - frontmatter: { - title: doc.title, - excerpt: doc.excerpt || '', - locale: doc.locale, - featuredImage: - typeof doc.featuredImage === 'object' && doc.featuredImage !== null - ? doc.featuredImage.sizes?.card?.url || doc.featuredImage.url - : null, - } as PageFrontmatter, - }; - }); + return docs.map((doc: any) => { + return { + slug: doc.slug, + frontmatter: { + title: doc.title, + excerpt: doc.excerpt || '', + locale: doc.locale, + featuredImage: + typeof doc.featuredImage === 'object' && doc.featuredImage !== null + ? doc.featuredImage.sizes?.card?.url || doc.featuredImage.url + : null, + } as PageFrontmatter, + }; + }); + } catch (error) { + console.error(`[Payload] getAllPagesMetadata failed for ${locale}:`, error); + return []; + } } diff --git a/next-env.d.ts b/next-env.d.ts index c4b7818f..9edff1c7 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/dev/types/routes.d.ts"; +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/package.json b/package.json index 5c37a1cd..a3392bbf 100644 --- a/package.json +++ b/package.json @@ -136,7 +136,7 @@ "prepare": "husky", "preinstall": "npx only-allow pnpm" }, - "version": "2.0.0", + "version": "2.0.1", "pnpm": { "onlyBuiltDependencies": [ "@parcel/watcher",