feat: payload cms
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Failing after 1m13s
Build & Deploy / 🏗️ Build (push) Failing after 5m53s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Smoke Test (push) Has been skipped
Build & Deploy / ⚡ Lighthouse (push) Has been skipped
Build & Deploy / ♿ WCAG (push) Has been skipped
Build & Deploy / 🛡️ Quality Gates (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 4s
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Failing after 1m13s
Build & Deploy / 🏗️ Build (push) Failing after 5m53s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Smoke Test (push) Has been skipped
Build & Deploy / ⚡ Lighthouse (push) Has been skipped
Build & Deploy / ♿ WCAG (push) Has been skipped
Build & Deploy / 🛡️ Quality Gates (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 4s
This commit is contained in:
285
lib/mdx.ts
285
lib/mdx.ts
@@ -1,6 +1,5 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import matter from 'gray-matter';
|
||||
import { getPayload } from 'payload';
|
||||
import configPromise from '@payload-config';
|
||||
import { mapSlugToFileSlug } from './slugs';
|
||||
|
||||
export interface ProductFrontmatter {
|
||||
@@ -10,65 +9,69 @@ export interface ProductFrontmatter {
|
||||
categories: string[];
|
||||
images: string[];
|
||||
locale: string;
|
||||
isFallback?: boolean;
|
||||
}
|
||||
|
||||
export interface ProductMdx {
|
||||
slug: string;
|
||||
frontmatter: ProductFrontmatter;
|
||||
content: string;
|
||||
content: any; // Lexical AST from Payload
|
||||
}
|
||||
|
||||
export async function getProductMetadata(
|
||||
slug: string,
|
||||
locale: string,
|
||||
): Promise<Partial<ProductMdx> | null> {
|
||||
// Map translated slug to file slug
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
const fileSlug = await mapSlugToFileSlug(slug, locale);
|
||||
const productsDir = path.join(process.cwd(), 'data', 'products', locale);
|
||||
|
||||
if (!fs.existsSync(productsDir)) return null;
|
||||
let result = await payload.find({
|
||||
collection: 'products',
|
||||
where: {
|
||||
and: [{ slug: { equals: fileSlug } }, { locale: { equals: locale } }],
|
||||
},
|
||||
depth: 1, // To auto-resolve Media relation (images array)
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
// Recursive search for the file
|
||||
const findFile = (dir: string): string | null => {
|
||||
const files = fs.readdirSync(dir);
|
||||
for (const file of files) {
|
||||
const fullPath = path.join(dir, file);
|
||||
const stat = fs.statSync(fullPath);
|
||||
if (stat.isDirectory()) {
|
||||
const found = findFile(fullPath);
|
||||
if (found) return found;
|
||||
} else if (file === `${fileSlug}.mdx` || file === `${fileSlug}-2.mdx`) {
|
||||
return fullPath;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
let isFallback = false;
|
||||
|
||||
let filePath = findFile(productsDir);
|
||||
|
||||
if (!filePath && locale !== 'en') {
|
||||
if (result.docs.length === 0 && locale !== 'en') {
|
||||
// Fallback to English
|
||||
const enProductsDir = path.join(process.cwd(), 'data', 'products', 'en');
|
||||
if (fs.existsSync(enProductsDir)) {
|
||||
filePath = findFile(enProductsDir);
|
||||
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 (filePath && fs.existsSync(filePath)) {
|
||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||
const { data } = matter(fileContent);
|
||||
if (result.docs.length > 0) {
|
||||
const doc = result.docs[0];
|
||||
|
||||
// Filter out products without images to match getProductBySlug behavior
|
||||
if (!data.images || data.images.length === 0 || !data.images[0]) {
|
||||
return null;
|
||||
}
|
||||
// Process Images
|
||||
const resolvedImages = ((doc.images as any[]) || [])
|
||||
.map((img) => (typeof img === 'string' ? img : img.url))
|
||||
.filter(Boolean);
|
||||
|
||||
if (resolvedImages.length === 0) return null; // Original logic skipped docs without images
|
||||
|
||||
return {
|
||||
slug: fileSlug,
|
||||
slug: doc.slug,
|
||||
frontmatter: {
|
||||
...data,
|
||||
isFallback: filePath.includes('/en/'),
|
||||
} as any,
|
||||
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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -76,111 +79,159 @@ export async function getProductMetadata(
|
||||
}
|
||||
|
||||
export async function getProductBySlug(slug: string, locale: string): Promise<ProductMdx | null> {
|
||||
// Map translated slug to file slug
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
const fileSlug = await mapSlugToFileSlug(slug, locale);
|
||||
const productsDir = path.join(process.cwd(), 'data', 'products', locale);
|
||||
|
||||
if (!fs.existsSync(productsDir)) return null;
|
||||
let result = await payload.find({
|
||||
collection: 'products',
|
||||
where: {
|
||||
and: [{ slug: { equals: fileSlug } }, { locale: { equals: locale } }],
|
||||
},
|
||||
depth: 1, // Auto-resolve Media logic
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
// Recursive search for the file
|
||||
const findFile = (dir: string): string | null => {
|
||||
const files = fs.readdirSync(dir);
|
||||
for (const file of files) {
|
||||
const fullPath = path.join(dir, file);
|
||||
const stat = fs.statSync(fullPath);
|
||||
if (stat.isDirectory()) {
|
||||
const found = findFile(fullPath);
|
||||
if (found) return found;
|
||||
} else if (file === `${fileSlug}.mdx` || file === `${fileSlug}-2.mdx`) {
|
||||
return fullPath;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
let filePath = findFile(productsDir);
|
||||
let isFallback = false;
|
||||
|
||||
if (!filePath && locale !== 'en') {
|
||||
if (result.docs.length === 0 && locale !== 'en') {
|
||||
// Fallback to English
|
||||
const enProductsDir = path.join(process.cwd(), 'data', 'products', 'en');
|
||||
if (fs.existsSync(enProductsDir)) {
|
||||
filePath = findFile(enProductsDir);
|
||||
if (filePath) isFallback = true;
|
||||
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 (filePath && fs.existsSync(filePath)) {
|
||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||
const { data, content } = matter(fileContent);
|
||||
const product = {
|
||||
slug: fileSlug,
|
||||
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: {
|
||||
...data,
|
||||
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,
|
||||
} as any,
|
||||
content,
|
||||
},
|
||||
content: doc.content, // Lexical payload instead of raw MDX String
|
||||
};
|
||||
|
||||
// Filter out products without images
|
||||
if (
|
||||
!product.frontmatter.images ||
|
||||
product.frontmatter.images.length === 0 ||
|
||||
!product.frontmatter.images[0]
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return product;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getAllProductSlugs(locale: string): Promise<string[]> {
|
||||
const productsDir = path.join(process.cwd(), 'data', 'products', locale);
|
||||
if (!fs.existsSync(productsDir)) return [];
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
const result = await payload.find({
|
||||
collection: 'products',
|
||||
where: {
|
||||
locale: {
|
||||
equals: locale,
|
||||
},
|
||||
},
|
||||
pagination: false, // get all docs
|
||||
});
|
||||
|
||||
const slugs: string[] = [];
|
||||
const walk = (dir: string) => {
|
||||
const files = fs.readdirSync(dir);
|
||||
for (const file of files) {
|
||||
const fullPath = path.join(dir, file);
|
||||
const stat = fs.statSync(fullPath);
|
||||
if (stat.isDirectory()) {
|
||||
walk(fullPath);
|
||||
} else if (file.endsWith('.mdx')) {
|
||||
slugs.push(file.replace(/\.mdx$/, ''));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
walk(productsDir);
|
||||
return slugs;
|
||||
return result.docs.map((doc) => doc.slug);
|
||||
}
|
||||
|
||||
export async function getAllProducts(locale: string): Promise<ProductMdx[]> {
|
||||
const slugs = await getAllProductSlugs(locale);
|
||||
let allSlugs = slugs;
|
||||
// Fetch ALL products in a single query to avoid N+1 getPayload() calls
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
|
||||
// Get products for this locale
|
||||
const result = await payload.find({
|
||||
collection: 'products',
|
||||
where: { locale: { equals: locale } },
|
||||
depth: 1,
|
||||
pagination: false,
|
||||
});
|
||||
|
||||
let products: ProductMdx[] = result.docs
|
||||
.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,
|
||||
},
|
||||
content: doc.content,
|
||||
}));
|
||||
|
||||
// Also include English fallbacks for slugs not in this locale
|
||||
if (locale !== 'en') {
|
||||
const enSlugs = await getAllProductSlugs('en');
|
||||
allSlugs = Array.from(new Set([...slugs, ...enSlugs]));
|
||||
const localeSlugs = new Set(products.map((p) => p.slug));
|
||||
const enResult = await payload.find({
|
||||
collection: 'products',
|
||||
where: { locale: { equals: 'en' } },
|
||||
depth: 1,
|
||||
pagination: false,
|
||||
});
|
||||
|
||||
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: doc.content,
|
||||
}));
|
||||
|
||||
products = [...products, ...fallbacks];
|
||||
}
|
||||
|
||||
const products = await Promise.all(allSlugs.map((slug) => getProductBySlug(slug, locale)));
|
||||
return products.filter((p): p is ProductMdx => p !== null);
|
||||
return products;
|
||||
}
|
||||
|
||||
export async function getAllProductsMetadata(locale: string): Promise<Partial<ProductMdx>[]> {
|
||||
const slugs = await getAllProductSlugs(locale);
|
||||
let allSlugs = slugs;
|
||||
|
||||
if (locale !== 'en') {
|
||||
const enSlugs = await getAllProductSlugs('en');
|
||||
allSlugs = Array.from(new Set([...slugs, ...enSlugs]));
|
||||
}
|
||||
|
||||
const metadata = await Promise.all(allSlugs.map((slug) => getProductMetadata(slug, locale)));
|
||||
return metadata.filter((m): m is Partial<ProductMdx> => m !== null);
|
||||
// Reuse getAllProducts to avoid separate N+1 queries
|
||||
const products = await getAllProducts(locale);
|
||||
return products.map((p) => ({
|
||||
slug: p.slug,
|
||||
frontmatter: p.frontmatter,
|
||||
}));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user