feat: payload cms
This commit is contained in:
14
lib/blog.ts
14
lib/blog.ts
@@ -35,7 +35,6 @@ export interface PostFrontmatter {
|
||||
focalX?: number;
|
||||
focalY?: number;
|
||||
category?: string;
|
||||
locale: string;
|
||||
public?: boolean;
|
||||
}
|
||||
|
||||
@@ -65,9 +64,9 @@ export async function getPostBySlug(slug: string, locale: string): Promise<PostM
|
||||
collection: 'posts',
|
||||
where: {
|
||||
slug: { equals: slug },
|
||||
locale: { equals: locale },
|
||||
...(!isDev ? { _status: { equals: 'published' } } : {}),
|
||||
},
|
||||
locale: locale as any,
|
||||
draft: isDev,
|
||||
limit: 1,
|
||||
});
|
||||
@@ -83,7 +82,6 @@ export async function getPostBySlug(slug: string, locale: string): Promise<PostM
|
||||
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
|
||||
@@ -113,11 +111,9 @@ export async function getAllPosts(locale: string): Promise<PostMdx[]> {
|
||||
const { docs } = await payload.find({
|
||||
collection: 'posts',
|
||||
where: {
|
||||
locale: {
|
||||
equals: locale,
|
||||
},
|
||||
...(!isDev ? { _status: { equals: 'published' } } : {}),
|
||||
},
|
||||
locale: locale as any,
|
||||
sort: '-date',
|
||||
draft: isDev,
|
||||
limit: 100,
|
||||
@@ -125,7 +121,7 @@ export async function getAllPosts(locale: string): Promise<PostMdx[]> {
|
||||
|
||||
console.log(`[Payload] getAllPosts for ${locale}: Found ${docs.length} docs`);
|
||||
|
||||
return docs.map((doc) => {
|
||||
const posts = docs.map((doc) => {
|
||||
return {
|
||||
slug: doc.slug,
|
||||
frontmatter: {
|
||||
@@ -133,7 +129,6 @@ export async function getAllPosts(locale: string): Promise<PostMdx[]> {
|
||||
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
|
||||
@@ -151,6 +146,9 @@ export async function getAllPosts(locale: string): Promise<PostMdx[]> {
|
||||
content: doc.content as any,
|
||||
};
|
||||
});
|
||||
|
||||
// Integrity check: only show posts with a featured image in listings/sitemap
|
||||
return posts.filter((p) => !!p.frontmatter.featuredImage);
|
||||
} catch (error) {
|
||||
console.error(`[Payload] getAllPosts failed for ${locale}:`, error);
|
||||
return [];
|
||||
|
||||
@@ -33,6 +33,17 @@ interface SendEmailOptions {
|
||||
|
||||
export async function sendEmail({ to, replyTo, subject, html }: SendEmailOptions) {
|
||||
const recipients = to || config.mail.recipients;
|
||||
const logger = getServerAppServices().logger.child({ component: 'mailer' });
|
||||
|
||||
if (!recipients) {
|
||||
logger.error('No email recipients configured (MAIL_RECIPIENTS is empty and no "to" provided)', { subject });
|
||||
return { success: false as const, error: 'No recipients configured' };
|
||||
}
|
||||
|
||||
if (!config.mail.from) {
|
||||
logger.error('MAIL_FROM is not configured — cannot send email', { subject, recipients });
|
||||
return { success: false as const, error: 'MAIL_FROM is not configured' };
|
||||
}
|
||||
|
||||
const mailOptions = {
|
||||
from: config.mail.from,
|
||||
@@ -42,7 +53,6 @@ export async function sendEmail({ to, replyTo, subject, html }: SendEmailOptions
|
||||
html,
|
||||
};
|
||||
|
||||
const logger = getServerAppServices().logger.child({ component: 'mailer' });
|
||||
|
||||
try {
|
||||
const info = await getTransporter().sendMail(mailOptions);
|
||||
|
||||
113
lib/pages.ts
113
lib/pages.ts
@@ -5,7 +5,9 @@ export interface PageFrontmatter {
|
||||
title: string;
|
||||
excerpt: string;
|
||||
featuredImage: string | null;
|
||||
locale: string;
|
||||
focalX?: number;
|
||||
focalY?: number;
|
||||
layout?: 'default' | 'fullBleed';
|
||||
public?: boolean;
|
||||
}
|
||||
|
||||
@@ -15,6 +17,30 @@ export interface PageMdx {
|
||||
content: any; // Lexical AST Document
|
||||
}
|
||||
|
||||
function mapDoc(doc: any): PageMdx {
|
||||
return {
|
||||
slug: doc.slug,
|
||||
frontmatter: {
|
||||
title: doc.title,
|
||||
excerpt: doc.excerpt || '',
|
||||
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,
|
||||
layout: doc.layout || 'default',
|
||||
} as PageFrontmatter,
|
||||
content: doc.content as any,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getPageBySlug(slug: string, locale: string): Promise<PageMdx | null> {
|
||||
try {
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
@@ -23,30 +49,14 @@ export async function getPageBySlug(slug: string, locale: string): Promise<PageM
|
||||
collection: 'pages' as any,
|
||||
where: {
|
||||
slug: { equals: slug },
|
||||
locale: { equals: locale },
|
||||
},
|
||||
locale: locale as any,
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
const docs = result.docs as any[];
|
||||
|
||||
if (!docs || docs.length === 0) return null;
|
||||
|
||||
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 mapDoc(docs[0]);
|
||||
} catch (error) {
|
||||
console.error(`[Payload] getPageBySlug failed for ${slug}:`, error);
|
||||
return null;
|
||||
@@ -59,31 +69,11 @@ export async function getAllPages(locale: string): Promise<PageMdx[]> {
|
||||
|
||||
const result = await payload.find({
|
||||
collection: 'pages' as any,
|
||||
where: {
|
||||
locale: {
|
||||
equals: locale,
|
||||
},
|
||||
},
|
||||
locale: locale as any,
|
||||
limit: 100,
|
||||
});
|
||||
|
||||
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 (result.docs as any[]).map(mapDoc);
|
||||
} catch (error) {
|
||||
console.error(`[Payload] getAllPages failed for ${locale}:`, error);
|
||||
return [];
|
||||
@@ -96,30 +86,29 @@ export async function getAllPagesMetadata(locale: string): Promise<Partial<PageM
|
||||
|
||||
const result = await payload.find({
|
||||
collection: 'pages' as any,
|
||||
where: {
|
||||
locale: {
|
||||
equals: locale,
|
||||
},
|
||||
},
|
||||
locale: locale as any,
|
||||
limit: 100,
|
||||
});
|
||||
|
||||
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 (result.docs as any[]).map((doc: any) => ({
|
||||
slug: doc.slug,
|
||||
frontmatter: {
|
||||
title: doc.title,
|
||||
excerpt: doc.excerpt || '',
|
||||
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 PageFrontmatter,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error(`[Payload] getAllPagesMetadata failed for ${locale}:`, error);
|
||||
return [];
|
||||
|
||||
@@ -8,11 +8,12 @@ export interface ProductFrontmatter {
|
||||
description: string;
|
||||
categories: string[];
|
||||
images: string[];
|
||||
locale: string;
|
||||
focalX?: number;
|
||||
focalY?: number;
|
||||
isFallback?: boolean;
|
||||
}
|
||||
|
||||
export interface ProductMdx {
|
||||
export interface ProductData {
|
||||
slug: string;
|
||||
frontmatter: ProductFrontmatter;
|
||||
content: any; // Lexical AST from Payload
|
||||
@@ -21,36 +22,24 @@ export interface ProductMdx {
|
||||
export async function getProductMetadata(
|
||||
slug: string,
|
||||
locale: string,
|
||||
): Promise<Partial<ProductMdx> | null> {
|
||||
): Promise<Partial<ProductData> | null> {
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
const fileSlug = await mapSlugToFileSlug(slug, locale);
|
||||
|
||||
let result = await payload.find({
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
const result = await payload.find({
|
||||
collection: 'products',
|
||||
where: {
|
||||
and: [{ slug: { equals: fileSlug } }, { locale: { equals: locale } }],
|
||||
and: [
|
||||
{ slug: { equals: fileSlug } },
|
||||
...(!isDev ? [{ _status: { equals: 'published' } }] : []),
|
||||
],
|
||||
},
|
||||
depth: 1, // To auto-resolve Media relation (images array)
|
||||
locale: locale as any,
|
||||
depth: 1,
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
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];
|
||||
|
||||
@@ -69,8 +58,6 @@ export async function getProductMetadata(
|
||||
description: doc.description,
|
||||
categories: Array.isArray(doc.categories) ? doc.categories.map((c: any) => c.category) : [],
|
||||
images: resolvedImages,
|
||||
locale: doc.locale,
|
||||
isFallback,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -78,37 +65,25 @@ export async function getProductMetadata(
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getProductBySlug(slug: string, locale: string): Promise<ProductMdx | null> {
|
||||
export async function getProductBySlug(slug: string, locale: string): Promise<ProductData | null> {
|
||||
try {
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
const fileSlug = await mapSlugToFileSlug(slug, locale);
|
||||
|
||||
let result = await payload.find({
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
const result = await payload.find({
|
||||
collection: 'products',
|
||||
where: {
|
||||
and: [{ slug: { equals: fileSlug } }, { locale: { equals: locale } }],
|
||||
and: [
|
||||
{ slug: { equals: fileSlug } },
|
||||
...(!isDev ? [{ _status: { equals: 'published' } }] : []),
|
||||
],
|
||||
},
|
||||
depth: 1, // Auto-resolve Media logic
|
||||
locale: locale as any,
|
||||
depth: 1,
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
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];
|
||||
|
||||
@@ -129,10 +104,16 @@ export async function getProductBySlug(slug: string, locale: string): Promise<Pr
|
||||
? doc.categories.map((c: any) => c.category)
|
||||
: [],
|
||||
images: resolvedImages,
|
||||
locale: doc.locale,
|
||||
isFallback,
|
||||
focalX:
|
||||
Array.isArray(doc.images) && doc.images.length > 0 && typeof doc.images[0] === 'object'
|
||||
? doc.images[0].focalX
|
||||
: 50,
|
||||
focalY:
|
||||
Array.isArray(doc.images) && doc.images.length > 0 && typeof doc.images[0] === 'object'
|
||||
? doc.images[0].focalY
|
||||
: 50,
|
||||
},
|
||||
content: doc.content, // Lexical payload instead of raw MDX String
|
||||
content: doc.content,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -146,14 +127,14 @@ export async function getProductBySlug(slug: string, locale: string): Promise<Pr
|
||||
export async function getAllProductSlugs(locale: string): Promise<string[]> {
|
||||
try {
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
const result = await payload.find({
|
||||
collection: 'products',
|
||||
where: {
|
||||
locale: {
|
||||
equals: locale,
|
||||
},
|
||||
...(!isDev ? { _status: { equals: 'published' } } : {}),
|
||||
},
|
||||
pagination: false, // get all docs
|
||||
locale: locale as any,
|
||||
pagination: false,
|
||||
});
|
||||
|
||||
return result.docs.map((doc) => doc.slug);
|
||||
@@ -163,7 +144,7 @@ export async function getAllProductSlugs(locale: string): Promise<string[]> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAllProducts(locale: string): Promise<ProductMdx[]> {
|
||||
export async function getAllProducts(locale: string): Promise<ProductData[]> {
|
||||
try {
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
|
||||
@@ -174,13 +155,15 @@ export async function getAllProducts(locale: string): Promise<ProductMdx[]> {
|
||||
description: true,
|
||||
categories: true,
|
||||
images: true,
|
||||
locale: true,
|
||||
} as const;
|
||||
|
||||
// Get products for this locale
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
const result = await payload.find({
|
||||
collection: 'products',
|
||||
where: { locale: { equals: locale } },
|
||||
where: {
|
||||
...(!isDev ? { _status: { equals: 'published' } } : {}),
|
||||
},
|
||||
locale: locale as any,
|
||||
depth: 1,
|
||||
pagination: false,
|
||||
select: selectFields,
|
||||
@@ -188,7 +171,7 @@ export async function getAllProducts(locale: string): Promise<ProductMdx[]> {
|
||||
|
||||
console.log(`[Payload] getAllProducts for ${locale}: Found ${result.docs.length} docs`);
|
||||
|
||||
let products: ProductMdx[] = result.docs.map((doc) => {
|
||||
let products: ProductData[] = result.docs.map((doc) => {
|
||||
const resolvedImages = ((doc.images as any[]) || [])
|
||||
.map((img) => (typeof img === 'string' ? img : img.url))
|
||||
.filter(Boolean) as string[];
|
||||
@@ -205,55 +188,21 @@ export async function getAllProducts(locale: string): Promise<ProductMdx[]> {
|
||||
description: doc.description ? String(doc.description) : '',
|
||||
categories: plainCategories,
|
||||
images: resolvedImages,
|
||||
locale: String(doc.locale),
|
||||
focalX:
|
||||
Array.isArray(doc.images) && doc.images.length > 0 && typeof doc.images[0] === 'object'
|
||||
? doc.images[0].focalX
|
||||
: 50,
|
||||
focalY:
|
||||
Array.isArray(doc.images) && doc.images.length > 0 && typeof doc.images[0] === 'object'
|
||||
? doc.images[0].focalY
|
||||
: 50,
|
||||
},
|
||||
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({
|
||||
collection: 'products',
|
||||
where: { locale: { equals: 'en' } },
|
||||
depth: 1,
|
||||
pagination: false,
|
||||
select: selectFields,
|
||||
});
|
||||
|
||||
console.log(
|
||||
`[Payload] getAllProducts (en fallbacks) for ${locale}: Found ${enResult.docs.length} docs`,
|
||||
);
|
||||
|
||||
const fallbacks = enResult.docs
|
||||
.filter((doc) => !localeSlugs.has(doc.slug))
|
||||
.map((doc) => {
|
||||
const resolvedImages = ((doc.images as any[]) || [])
|
||||
.map((img) => (typeof img === 'string' ? img : img.url))
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
const plainCategories = Array.isArray(doc.categories)
|
||||
? doc.categories.map((c: any) => String(c.category))
|
||||
: [];
|
||||
|
||||
return {
|
||||
slug: String(doc.slug),
|
||||
frontmatter: {
|
||||
title: String(doc.title),
|
||||
sku: doc.sku ? String(doc.sku) : '',
|
||||
description: doc.description ? String(doc.description) : '',
|
||||
categories: plainCategories,
|
||||
images: resolvedImages,
|
||||
locale: String(doc.locale),
|
||||
isFallback: true,
|
||||
},
|
||||
content: null,
|
||||
};
|
||||
});
|
||||
|
||||
products = [...products, ...fallbacks];
|
||||
}
|
||||
// Filter out products with 0 images (data integrity check to prevent 404s)
|
||||
products = products.filter((p) => p.frontmatter.images && p.frontmatter.images.length > 0);
|
||||
|
||||
return products;
|
||||
} catch (error) {
|
||||
@@ -262,7 +211,7 @@ export async function getAllProducts(locale: string): Promise<ProductMdx[]> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAllProductsMetadata(locale: string): Promise<Partial<ProductMdx>[]> {
|
||||
export async function getAllProductsMetadata(locale: string): Promise<Partial<ProductData>[]> {
|
||||
const products = await getAllProducts(locale);
|
||||
return products.map((p) => ({
|
||||
slug: p.slug,
|
||||
Reference in New Issue
Block a user