fix: resolve layout, pdf datasheets data generation and excel 404 routing

This commit is contained in:
2026-03-03 12:38:55 +01:00
parent 34bb91c04b
commit 655f33091f
105 changed files with 1337 additions and 1077 deletions

View File

@@ -21,447 +21,490 @@ import { getDatasheetPath } from '../lib/datasheets';
import { mapFileSlugToTranslated } from '../lib/slugs';
const CONFIG = {
outputDir: path.join(process.cwd(), 'public/brochure'),
host: process.env.NEXT_PUBLIC_SITE_URL || 'https://klz-cables.com',
outputDir: path.join(process.cwd(), 'public/brochure'),
host: process.env.NEXT_PUBLIC_SITE_URL || 'https://klz-cables.com',
} as const;
// ─── Helpers ────────────────────────────────────────────────────────────────
async function resolveImage(url: string): Promise<string | Buffer> {
if (!url) return '';
let localPath = '';
// If it's a Payload media URL like /api/media/file/filename.ext
if (url.startsWith('/api/media/file/')) {
const filename = url.replace('/api/media/file/', '');
localPath = path.join(process.cwd(), 'public/media', filename);
} else if (url.startsWith('/media/')) {
localPath = path.join(process.cwd(), 'public', url);
}
if (!url) return '';
let localPath = '';
// If it's a Payload media URL like /api/media/file/filename.ext
if (url.startsWith('/api/media/file/')) {
const filename = url.replace('/api/media/file/', '');
localPath = path.join(process.cwd(), 'public/media', filename);
} else if (url.startsWith('/media/')) {
localPath = path.join(process.cwd(), 'public', url);
}
if (localPath && fs.existsSync(localPath)) {
// If it's webp, convert to png buffer for react-pdf
if (localPath.toLowerCase().endsWith('.webp')) {
try {
return await sharp(localPath).png().toBuffer();
} catch (err) {
return localPath;
}
}
if (localPath && fs.existsSync(localPath)) {
// If it's webp, convert to png buffer for react-pdf
if (localPath.toLowerCase().endsWith('.webp')) {
try {
return await sharp(localPath).png().toBuffer();
} catch (err) {
return localPath;
}
}
// Fallback to absolute URL if starting with /
if (url.startsWith('/')) return `${CONFIG.host}${url}`;
return url;
return localPath;
}
// Fallback to absolute URL if starting with /
if (url.startsWith('/')) return `${CONFIG.host}${url}`;
return url;
}
function stripHtml(html: string): string {
if (!html) return '';
return html.replace(/<[^>]*>/g, '').trim();
if (!html) return '';
return html.replace(/<[^>]*>/g, '').trim();
}
function ensureOutputDir(): void {
if (!fs.existsSync(CONFIG.outputDir)) {
fs.mkdirSync(CONFIG.outputDir, { recursive: true });
}
if (!fs.existsSync(CONFIG.outputDir)) {
fs.mkdirSync(CONFIG.outputDir, { recursive: true });
}
}
async function fetchQrCodeBuffer(url: string): Promise<Buffer | undefined> {
if (!url) return undefined;
try {
const qrApi = `https://api.qrserver.com/v1/create-qr-code/?size=80x80&data=${encodeURIComponent(url)}&margin=0`;
const res = await fetch(qrApi);
if (res.ok) {
const arrayBuffer = await res.arrayBuffer();
return Buffer.from(arrayBuffer);
} else {
console.error(` [QR] Failed (HTTP ${res.status}) for ${url}`);
}
} catch (err) {
console.error(` [QR] Failed for ${url}:`, err);
if (!url) return undefined;
try {
const qrApi = `https://api.qrserver.com/v1/create-qr-code/?size=80x80&data=${encodeURIComponent(url)}&margin=0`;
const res = await fetch(qrApi);
if (res.ok) {
const arrayBuffer = await res.arrayBuffer();
return Buffer.from(arrayBuffer);
} else {
console.error(` [QR] Failed (HTTP ${res.status}) for ${url}`);
}
return undefined;
} catch (err) {
console.error(` [QR] Failed for ${url}:`, err);
}
return undefined;
}
async function resolveLocalFile(relativePath: string): Promise<string | Buffer | undefined> {
const abs = path.join(process.cwd(), 'public', relativePath);
if (!fs.existsSync(abs)) return undefined;
if (abs.endsWith('.svg')) {
try {
const svgBuf = fs.readFileSync(abs);
return await sharp(svgBuf).resize(600).png().toBuffer();
} catch { return abs; }
const abs = path.join(process.cwd(), 'public', relativePath);
if (!fs.existsSync(abs)) return undefined;
if (abs.endsWith('.svg')) {
try {
const svgBuf = fs.readFileSync(abs);
return await sharp(svgBuf).resize(600).png().toBuffer();
} catch {
return abs;
}
return abs;
}
return abs;
}
// ─── CMS Product Loading ────────────────────────────────────────────────────
async function loadProducts(locale: 'en' | 'de'): Promise<BrochureProduct[]> {
const products: BrochureProduct[] = [];
try {
const payload = await getPayload({ config: configPromise });
const isDev = process.env.NODE_ENV === 'development';
const products: BrochureProduct[] = [];
try {
const payload = await getPayload({ config: configPromise });
const isDev = process.env.NODE_ENV === 'development';
const result = await payload.find({
collection: 'products',
where: {
...(!isDev ? { _status: { equals: 'published' } } : {}),
},
locale: locale as any,
pagination: false,
});
const result = await payload.find({
collection: 'products',
where: {
...(!isDev ? { _status: { equals: 'published' } } : {}),
},
locale: locale as any,
pagination: false,
});
const productsSlug = await mapFileSlugToTranslated('products', locale);
const productsSlug = await mapFileSlugToTranslated('products', locale);
let id = 1;
for (const doc of result.docs) {
if (!doc.title || !doc.slug) continue;
let id = 1;
for (const doc of result.docs) {
if (!doc.title || !doc.slug) continue;
const images: any[] = [];
const rawImages: string[] = [];
const images: any[] = [];
const rawImages: string[] = [];
if (doc.featuredImage) {
const url = typeof doc.featuredImage === 'string' ? doc.featuredImage : (doc.featuredImage as any).url;
if (url) rawImages.push(url);
}
if (Array.isArray(doc.images)) {
for (const img of doc.images) {
const url = typeof img === 'string' ? img : (img as any).url;
if (url && !rawImages.includes(url)) rawImages.push(url);
}
}
for (const url of rawImages) {
const resolved = await resolveImage(url);
if (resolved) images.push(resolved);
}
const attributes: any[] = [];
// Extract basic technical attributes from Lexical AST
if (Array.isArray(doc.content?.root?.children)) {
const productTabsBlock = doc.content.root.children.find(
(node: any) => node.type === 'block' && node.fields?.blockType === 'productTabs'
);
if (productTabsBlock && productTabsBlock.fields) {
if (Array.isArray(productTabsBlock.fields.technicalItems)) {
for (const item of productTabsBlock.fields.technicalItems) {
const label = item.unit ? `${item.label} [${item.unit}]` : item.label;
if (label && item.value) {
attributes.push({ name: label, options: [item.value] });
}
}
}
if (Array.isArray(productTabsBlock.fields.voltageTables)) {
for (const vt of productTabsBlock.fields.voltageTables) {
if (vt.voltageLabel) {
attributes.push({ name: 'Voltage', options: [vt.voltageLabel] });
}
}
}
}
}
const categories = Array.isArray(doc.categories)
? doc.categories.map((c: any) => ({ name: String(c.category || c), slug: String(c.slug || c) })).filter((c: any) => c.name)
: [];
// Compute QR URLs
let qrWebsiteUrl = '';
if (categories.length > 0 && categories[0].slug) {
const catTranslatedSlug = await mapFileSlugToTranslated(categories[0].slug, locale);
qrWebsiteUrl = `${CONFIG.host}/${locale}/${productsSlug}/${catTranslatedSlug}/${doc.slug}`;
}
let qrDatasheetUrl = '';
const datasheetRelativePath = getDatasheetPath(String(doc.slug), locale);
if (datasheetRelativePath) {
qrDatasheetUrl = `${CONFIG.host}${datasheetRelativePath}`;
}
const [qrWebsite, qrDatasheet] = await Promise.all([
qrWebsiteUrl ? fetchQrCodeBuffer(qrWebsiteUrl) : Promise.resolve(undefined),
qrDatasheetUrl ? fetchQrCodeBuffer(qrDatasheetUrl) : Promise.resolve(undefined),
]);
products.push({
id: id++,
name: String(doc.title),
slug: String(doc.slug),
sku: String(doc.sku || ''),
shortDescriptionHtml: '',
descriptionHtml: stripHtml(String(doc.description || '')),
images: images as any, // mix of paths and buffers
featuredImage: images[0] || null,
categories,
attributes,
qrWebsite,
qrDatasheet,
});
console.log(` - ${doc.title} (QR: ${qrWebsite ? 'Web ' : ''}${qrDatasheet ? 'PDF' : ''})`);
if (doc.featuredImage) {
const url =
typeof doc.featuredImage === 'string'
? doc.featuredImage
: (doc.featuredImage as any).url;
if (url) rawImages.push(url);
}
if (Array.isArray(doc.images)) {
for (const img of doc.images) {
const url = typeof img === 'string' ? img : (img as any).url;
if (url && !rawImages.includes(url)) rawImages.push(url);
}
} catch (error) {
console.error(`[Payload] Failed to fetch products (${locale}):`, error);
}
for (const url of rawImages) {
const resolved = await resolveImage(url);
if (resolved) images.push(resolved);
}
const attributes: any[] = [];
// Extract basic technical attributes from Lexical AST if present
if (Array.isArray(doc.content?.root?.children)) {
const productTabsBlock = doc.content.root.children.find(
(node: any) => node.type === 'block' && node.fields?.blockType === 'productTabs',
);
if (productTabsBlock && productTabsBlock.fields) {
if (Array.isArray(productTabsBlock.fields.technicalItems)) {
for (const item of productTabsBlock.fields.technicalItems) {
const label = item.unit ? `${item.label} [${item.unit}]` : item.label;
if (label && item.value) {
attributes.push({
name: label,
options: [String(item.value)],
});
}
}
}
}
}
const categories = Array.isArray(doc.categories)
? doc.categories
.map((c: any) => ({ name: String(c.category || c), slug: String(c.slug || c) }))
.filter((c: any) => c.name)
: [];
// Compute QR URLs
let qrWebsiteUrl = '';
if (categories.length > 0 && categories[0].slug) {
const catTranslatedSlug = await mapFileSlugToTranslated(categories[0].slug, locale);
qrWebsiteUrl = `${CONFIG.host}/${locale}/${productsSlug}/${catTranslatedSlug}/${doc.slug}`;
}
let qrDatasheetUrl = '';
const datasheetRelativePath = getDatasheetPath(String(doc.slug), locale);
if (datasheetRelativePath) {
qrDatasheetUrl = `${CONFIG.host}${datasheetRelativePath}`;
}
const [qrWebsite, qrDatasheet] = await Promise.all([
qrWebsiteUrl ? fetchQrCodeBuffer(qrWebsiteUrl) : Promise.resolve(undefined),
qrDatasheetUrl ? fetchQrCodeBuffer(qrDatasheetUrl) : Promise.resolve(undefined),
]);
products.push({
id: id++,
name: String(doc.title),
slug: String(doc.slug),
sku: String(doc.sku || ''),
shortDescriptionHtml: '',
descriptionHtml: stripHtml(String(doc.description || '')),
images: images as any, // mix of paths and buffers
featuredImage: images[0] || null,
categories,
attributes,
qrWebsite,
qrDatasheet,
});
console.log(` - ${doc.title} (QR: ${qrWebsite ? 'Web ' : ''}${qrDatasheet ? 'PDF' : ''})`);
}
} catch (error) {
console.error(`[Payload] Failed to fetch products (${locale}):`, error);
}
products.sort((a, b) => a.name.localeCompare(b.name));
products.sort((a, b) => a.name.localeCompare(b.name));
// FILTER: Only include products that have images for the high-fidelity brochure
const filteredProducts = products.filter(p => p.images.length > 0 || p.featuredImage);
console.log(` Filtered: ${filteredProducts.length} products with images (out of ${products.length})`);
// FILTER: Only include products that have images for the high-fidelity brochure
const filteredProducts = products.filter((p) => p.images.length > 0 || p.featuredImage);
console.log(
` Filtered: ${filteredProducts.length} products with images (out of ${products.length})`,
);
return filteredProducts;
return filteredProducts;
}
// ─── CMS Start/Intro Page ───────────────────────────────────────────────────
async function loadIntroContent(locale: 'en' | 'de'): Promise<BrochureProps['introContent'] | undefined> {
try {
const payload = await getPayload({ config: configPromise });
const result = await payload.find({
collection: 'pages',
where: { slug: { equals: 'start' } },
locale: locale as any,
});
async function loadIntroContent(
locale: 'en' | 'de',
): Promise<BrochureProps['introContent'] | undefined> {
try {
const payload = await getPayload({ config: configPromise });
const result = await payload.find({
collection: 'pages',
where: { slug: { equals: 'start' } },
locale: locale as any,
});
if (result.docs.length > 0) {
const doc = result.docs[0];
const heroUrl = typeof doc.featuredImage === 'string'
? doc.featuredImage
: (doc.featuredImage as any)?.url;
if (result.docs.length > 0) {
const doc = result.docs[0];
const heroUrl =
typeof doc.featuredImage === 'string' ? doc.featuredImage : (doc.featuredImage as any)?.url;
const heroImage = await resolveImage(heroUrl);
const heroImage = await resolveImage(heroUrl);
return {
title: String(doc.title),
excerpt: String(doc.excerpt || ''),
heroImage: heroImage as any,
};
}
} catch (error) {
console.error(`[Payload] Failed to fetch intro content (${locale}):`, error);
return {
title: String(doc.title),
excerpt: String(doc.excerpt || ''),
heroImage: heroImage as any,
};
}
return undefined;
} catch (error) {
console.error(`[Payload] Failed to fetch intro content (${locale}):`, error);
}
return undefined;
}
// ─── Marketing Sections ───────────────────────────────────────────────────
async function loadMarketingSections(locale: 'en' | 'de'): Promise<BrochureProps['marketingSections'] | undefined> {
try {
const messagesPath = path.join(process.cwd(), `messages/${locale}.json`);
const messagesJson = fs.readFileSync(messagesPath, 'utf-8');
const messages = JSON.parse(messagesJson);
async function loadMarketingSections(
locale: 'en' | 'de',
): Promise<BrochureProps['marketingSections'] | undefined> {
try {
const messagesPath = path.join(process.cwd(), `messages/${locale}.json`);
const messagesJson = fs.readFileSync(messagesPath, 'utf-8');
const messages = JSON.parse(messagesJson);
const sections: NonNullable<BrochureProps['marketingSections']> = [];
const sections: NonNullable<BrochureProps['marketingSections']> = [];
// ── 1. Was wir tun + Warum wir — MERGED into one compact section ──
{
const allItems: Array<{ title: string; description: string }> = [];
// ── 1. Was wir tun + Warum wir — MERGED into one compact section ──
{
const allItems: Array<{ title: string; description: string }> = [];
// WhatWeDo items — truncated to 1 sentence each
if (messages.Home?.whatWeDo?.items) {
for (const item of messages.Home.whatWeDo.items) {
allItems.push({
title: item.title.split('.')[0], // short title
description: item.description.split('.')[0] + '.',
});
}
}
// WhyChooseUs items — truncated to 1 sentence each
if (messages.Home?.whyChooseUs?.items) {
for (const item of messages.Home.whyChooseUs.items) {
allItems.push({
title: item.title,
description: item.description.split('.')[0] + '.',
});
}
}
sections.push({
title: messages.Home?.whatWeDo?.title || (locale === 'de' ? 'Was wir tun' : 'What We Do'),
subtitle: locale === 'de' ? 'Leistungen & Stärken' : 'Services & Strengths',
description: messages.Home?.whatWeDo?.subtitle || '',
items: allItems,
});
// WhatWeDo items — truncated to 1 sentence each
if (messages.Home?.whatWeDo?.items) {
for (const item of messages.Home.whatWeDo.items) {
allItems.push({
title: item.title.split('.')[0], // short title
description: item.description.split('.')[0] + '.',
});
}
// ── 2. Experience & Quality — merge Legacy + Experience highlights ──
{
const legacy = messages.Team?.legacy;
const experience = messages.Home?.experience;
const highlights: Array<{ value: string; label: string }> = [];
if (legacy) {
highlights.push(
{ value: legacy.expertise || 'Expertise', label: legacy.expertiseDesc || '' },
{ value: legacy.network || (locale === 'de' ? 'Netzwerk' : 'Network'), label: legacy.networkDesc || '' },
);
}
if (experience) {
highlights.push(
{ value: experience.certifiedQuality || (locale === 'de' ? 'Zertifiziert' : 'Certified'), label: experience.vdeApproved || '' },
{ value: experience.fullSpectrum || (locale === 'de' ? 'Volles Spektrum' : 'Full Spectrum'), label: experience.solutionsRange || '' },
);
}
const desc = legacy?.p1 || '';
sections.push({
title: legacy?.title || (locale === 'de' ? 'Erfahrung & Qualität' : 'Experience & Quality'),
subtitle: locale === 'de' ? 'Unser Erbe' : 'Our Heritage',
description: desc,
highlights,
});
}
// WhyChooseUs items — truncated to 1 sentence each
if (messages.Home?.whyChooseUs?.items) {
for (const item of messages.Home.whyChooseUs.items) {
allItems.push({
title: item.title,
description: item.description.split('.')[0] + '.',
});
}
}
return sections.length > 0 ? sections : undefined;
} catch (error) {
console.error(`[Messages] Failed to fetch marketing sections (${locale}):`, error);
sections.push({
title: messages.Home?.whatWeDo?.title || (locale === 'de' ? 'Was wir tun' : 'What We Do'),
subtitle: locale === 'de' ? 'Leistungen & Stärken' : 'Services & Strengths',
description: messages.Home?.whatWeDo?.subtitle || '',
items: allItems,
});
}
return undefined;
// ── 2. Experience & Quality — merge Legacy + Experience highlights ──
{
const legacy = messages.Team?.legacy;
const experience = messages.Home?.experience;
const highlights: Array<{ value: string; label: string }> = [];
if (legacy) {
highlights.push(
{ value: legacy.expertise || 'Expertise', label: legacy.expertiseDesc || '' },
{
value: legacy.network || (locale === 'de' ? 'Netzwerk' : 'Network'),
label: legacy.networkDesc || '',
},
);
}
if (experience) {
highlights.push(
{
value: experience.certifiedQuality || (locale === 'de' ? 'Zertifiziert' : 'Certified'),
label: experience.vdeApproved || '',
},
{
value:
experience.fullSpectrum || (locale === 'de' ? 'Volles Spektrum' : 'Full Spectrum'),
label: experience.solutionsRange || '',
},
);
}
const desc = legacy?.p1 || '';
sections.push({
title: legacy?.title || (locale === 'de' ? 'Erfahrung & Qualität' : 'Experience & Quality'),
subtitle: locale === 'de' ? 'Unser Erbe' : 'Our Heritage',
description: desc,
highlights,
});
}
return sections.length > 0 ? sections : undefined;
} catch (error) {
console.error(`[Messages] Failed to fetch marketing sections (${locale}):`, error);
}
return undefined;
}
// ─── Company Info ───────────────────────────────────────────────────────────
function getCompanyInfo(locale: 'en' | 'de'): BrochureProps['companyInfo'] {
const values = locale === 'de' ? [
{ title: 'Kompetenz', description: 'Jahrzehntelange Erfahrung und europaweites Know-how.' },
{ title: 'Verfügbarkeit', description: 'Immer für Sie da schnelle Unterstützung.' },
{ title: 'Lösungen', description: 'Wir finden die beste Kabellösung für Ihr Projekt.' },
{ title: 'Zuverlässigkeit', description: 'Wir halten, was wir versprechen.' },
] : [
{ title: 'Competence', description: 'Decades of experience and Europe-wide know-how.' },
{ title: 'Availability', description: 'Always there for you fast support.' },
{ title: 'Solutions', description: 'We find the best cable solution for your project.' },
{ title: 'Reliability', description: 'We deliver what we promise.' },
];
const values =
locale === 'de'
? [
{
title: 'Kompetenz',
description: 'Jahrzehntelange Erfahrung und europaweites Know-how.',
},
{ title: 'Verfügbarkeit', description: 'Immer für Sie da schnelle Unterstützung.' },
{ title: 'Lösungen', description: 'Wir finden die beste Kabellösung für Ihr Projekt.' },
{ title: 'Zuverlässigkeit', description: 'Wir halten, was wir versprechen.' },
]
: [
{ title: 'Competence', description: 'Decades of experience and Europe-wide know-how.' },
{ title: 'Availability', description: 'Always there for you fast support.' },
{ title: 'Solutions', description: 'We find the best cable solution for your project.' },
{ title: 'Reliability', description: 'We deliver what we promise.' },
];
return {
tagline: locale === 'de'
? 'Wegweisend in der Kabelinfrastruktur.'
: 'Leading the way in cable infrastructure.',
values,
address: 'Raiffeisenstraße 22, 73630 Remshalden, Germany',
phone: '+49 (0) 7151 959 89-0',
email: 'info@klz-cables.com',
website: 'www.klz-cables.com',
};
return {
tagline:
locale === 'de'
? 'Wegweisend in der Kabelinfrastruktur.'
: 'Leading the way in cable infrastructure.',
values,
address: 'Raiffeisenstraße 22, 73630 Remshalden, Germany',
phone: '+49 (0) 7151 959 89-0',
email: 'info@klz-cables.com',
website: 'www.klz-cables.com',
};
}
// ─── Main ───────────────────────────────────────────────────────────────────
async function main(): Promise<void> {
const start = Date.now();
console.log('Starting brochure generation (Full Brochure with website content)');
ensureOutputDir();
const start = Date.now();
console.log('Starting brochure generation (Full Brochure with website content)');
ensureOutputDir();
const locales: Array<'en' | 'de'> = ['en', 'de'];
const locales: Array<'en' | 'de'> = ['en', 'de'];
// Load the REAL logos (not the favicon/icon!)
const logoWhitePath = path.join(process.cwd(), 'public/logo-white.png');
const logoBlackPath = path.join(process.cwd(), 'public/logo-black.png');
const logoFallbackPath = path.join(process.cwd(), 'public/logo.png');
// Load the REAL logos (not the favicon/icon!)
const logoWhitePath = path.join(process.cwd(), 'public/logo-white.png');
const logoBlackPath = path.join(process.cwd(), 'public/logo-black.png');
const logoFallbackPath = path.join(process.cwd(), 'public/logo.png');
const logoWhite = fs.existsSync(logoWhitePath) ? logoWhitePath : undefined;
const logoBlack = fs.existsSync(logoBlackPath) ? logoBlackPath : (fs.existsSync(logoFallbackPath) ? logoFallbackPath : undefined);
const logoWhite = fs.existsSync(logoWhitePath) ? logoWhitePath : undefined;
const logoBlack = fs.existsSync(logoBlackPath)
? logoBlackPath
: fs.existsSync(logoFallbackPath)
? logoFallbackPath
: undefined;
console.log(`Logos: white=${!!logoWhite} black=${!!logoBlack}`);
console.log(`Logos: white=${!!logoWhite} black=${!!logoBlack}`);
// EXACT image mapping — 2 marketing sections now
// Index map: 0=Cover, 1=About, 2=WasWirTun(null), 3=Erfahrung(Legacy image), 4=BackCover
const galleryPaths: Array<string | null> = [
'uploads/2024/12/large-rolls-of-wires-against-the-blue-sky-at-sunse-2023-11-27-05-20-33-utc-Large.webp', // 0: Cover (cable drums, no people)
'uploads/2024/12/DSC07460-Large-600x400.webp', // 1: About section
null, // 2: Was wir tun (NO IMAGE — text-heavy)
'uploads/2024/12/1694273920124-copy.webp', // 3: Erfahrung & Qualität
'uploads/2024/12/DSC07433-Large-600x400.webp', // 4: Back cover
];
// EXACT image mapping — 2 marketing sections now
// Index map: 0=Cover, 1=About, 2=WasWirTun(null), 3=Erfahrung(Legacy image), 4=BackCover
const galleryPaths: Array<string | null> = [
'uploads/2024/12/large-rolls-of-wires-against-the-blue-sky-at-sunse-2023-11-27-05-20-33-utc-Large.webp', // 0: Cover (cable drums, no people)
'uploads/2024/12/DSC07460-Large-600x400.webp', // 1: About section
null, // 2: Was wir tun (NO IMAGE — text-heavy)
'uploads/2024/12/1694273920124-copy.webp', // 3: Erfahrung & Qualität
'uploads/2024/12/DSC07433-Large-600x400.webp', // 4: Back cover
];
const galleryImages: (string | Buffer | undefined)[] = [];
for (const gp of galleryPaths) {
if (!gp) {
galleryImages.push(undefined);
continue;
}
const fullPath = path.join(process.cwd(), 'public', gp);
if (fs.existsSync(fullPath)) {
try {
const buf = await sharp(fullPath).png({ quality: 80 }).resize(800).toBuffer();
galleryImages.push(buf);
} catch {
galleryImages.push(undefined);
}
} else {
galleryImages.push(undefined);
}
const galleryImages: (string | Buffer | undefined)[] = [];
for (const gp of galleryPaths) {
if (!gp) {
galleryImages.push(undefined);
continue;
}
console.log(`Gallery images mapping complete. Succeeded bindings: ${galleryImages.filter(b => b !== undefined).length}`);
const fullPath = path.join(process.cwd(), 'public', gp);
if (fs.existsSync(fullPath)) {
try {
const buf = await sharp(fullPath).png({ quality: 80 }).resize(800).toBuffer();
galleryImages.push(buf);
} catch {
galleryImages.push(undefined);
}
} else {
galleryImages.push(undefined);
}
}
console.log(
`Gallery images mapping complete. Succeeded bindings: ${galleryImages.filter((b) => b !== undefined).length}`,
);
for (const locale of locales) {
console.log(`\nGenerating ${locale.toUpperCase()} brochure...`);
const [products, introContent, marketingSections] = await Promise.all([
loadProducts(locale),
loadIntroContent(locale),
loadMarketingSections(locale)
]);
for (const locale of locales) {
console.log(`\nGenerating ${locale.toUpperCase()} brochure...`);
const [products, introContent, marketingSections] = await Promise.all([
loadProducts(locale),
loadIntroContent(locale),
loadMarketingSections(locale),
]);
if (products.length === 0) continue;
const companyInfo = getCompanyInfo(locale);
if (products.length === 0) continue;
const companyInfo = getCompanyInfo(locale);
// Load messages for About page content (directors, legacy, etc.)
let messages: Record<string, any> | undefined;
try {
const messagesPath = path.join(process.cwd(), `messages/${locale}.json`);
messages = JSON.parse(fs.readFileSync(messagesPath, 'utf-8'));
} catch { /* messages are optional */ }
// Load director portrait photos and crop to circles
const directorPhotos: { michael?: Buffer; klaus?: Buffer } = {};
const portraitPaths = {
michael: path.join(process.cwd(), 'public/uploads/2024/12/DSC07768-Large.webp'),
klaus: path.join(process.cwd(), 'public/uploads/2024/12/DSC07963-Large.webp'),
};
const AVATAR_SIZE = 120; // px, will be rendered at 32pt in PDF
const circleMask = Buffer.from(
`<svg width="${AVATAR_SIZE}" height="${AVATAR_SIZE}"><circle cx="${AVATAR_SIZE / 2}" cy="${AVATAR_SIZE / 2}" r="${AVATAR_SIZE / 2}" fill="white"/></svg>`
);
for (const [key, photoPath] of Object.entries(portraitPaths)) {
if (fs.existsSync(photoPath)) {
try {
const cropped = await sharp(photoPath)
.resize(AVATAR_SIZE, AVATAR_SIZE, { fit: 'cover', position: 'top' })
.composite([{ input: circleMask, blend: 'dest-in' }])
.png()
.toBuffer();
directorPhotos[key as 'michael' | 'klaus'] = cropped;
} catch { /* skip */ }
}
}
try {
// Render the React-PDF brochure
const buffer = await renderToBuffer(
React.createElement(PDFBrochure, {
products, locale, companyInfo, introContent,
marketingSections, logoBlack, logoWhite, galleryImages, messages, directorPhotos,
} as any) as any
);
// Write final PDF
const outPath = path.join(process.cwd(), `public/brochure/klz-product-catalog-${locale}.pdf`);
fs.mkdirSync(path.dirname(outPath), { recursive: true });
fs.writeFileSync(outPath, buffer);
const sizeKB = Math.round(buffer.length / 1024);
console.log(` ✓ Generated: klz-product-catalog-${locale}.pdf (${sizeKB} KB)`);
} catch (error) {
console.error(` ✗ Failed to generate ${locale} brochure:`, error);
}
// Load messages for About page content (directors, legacy, etc.)
let messages: Record<string, any> | undefined;
try {
const messagesPath = path.join(process.cwd(), `messages/${locale}.json`);
messages = JSON.parse(fs.readFileSync(messagesPath, 'utf-8'));
} catch {
/* messages are optional */
}
console.log(`\n✅ Done!`);
console.log(`Output: ${CONFIG.outputDir}`);
console.log(`Time: ${((Date.now() - start) / 1000).toFixed(2)}s`);
// Load director portrait photos and crop to circles
const directorPhotos: { michael?: Buffer; klaus?: Buffer } = {};
const portraitPaths = {
michael: path.join(process.cwd(), 'public/uploads/2024/12/DSC07768-Large.webp'),
klaus: path.join(process.cwd(), 'public/uploads/2024/12/DSC07963-Large.webp'),
};
const AVATAR_SIZE = 120; // px, will be rendered at 32pt in PDF
const circleMask = Buffer.from(
`<svg width="${AVATAR_SIZE}" height="${AVATAR_SIZE}"><circle cx="${AVATAR_SIZE / 2}" cy="${AVATAR_SIZE / 2}" r="${AVATAR_SIZE / 2}" fill="white"/></svg>`,
);
for (const [key, photoPath] of Object.entries(portraitPaths)) {
if (fs.existsSync(photoPath)) {
try {
const cropped = await sharp(photoPath)
.resize(AVATAR_SIZE, AVATAR_SIZE, { fit: 'cover', position: 'top' })
.composite([{ input: circleMask, blend: 'dest-in' }])
.png()
.toBuffer();
directorPhotos[key as 'michael' | 'klaus'] = cropped;
} catch {
/* skip */
}
}
}
try {
// Render the React-PDF brochure
const buffer = await renderToBuffer(
React.createElement(PDFBrochure, {
products,
locale,
companyInfo,
introContent,
marketingSections,
logoBlack,
logoWhite,
galleryImages,
messages,
directorPhotos,
} as any) as any,
);
// Write final PDF
const outPath = path.join(process.cwd(), `public/brochure/klz-product-catalog-${locale}.pdf`);
fs.mkdirSync(path.dirname(outPath), { recursive: true });
fs.writeFileSync(outPath, buffer);
const sizeKB = Math.round(buffer.length / 1024);
console.log(` ✓ Generated: klz-product-catalog-${locale}.pdf (${sizeKB} KB)`);
} catch (error) {
console.error(` ✗ Failed to generate ${locale} brochure:`, error);
}
}
console.log(`\n✅ Done!`);
console.log(`Output: ${CONFIG.outputDir}`);
console.log(`Time: ${((Date.now() - start) / 1000).toFixed(2)}s`);
}
main().catch(console.error);

View File

@@ -30,10 +30,10 @@ const ASSET_MAP_FILE = path.join(process.cwd(), 'data/processed/asset-map.json')
const PUBLIC_DIR = path.join(process.cwd(), 'public');
const EXCEL_SOURCE_FILES = [
path.join(process.cwd(), 'data/source/high-voltage.xlsx'),
path.join(process.cwd(), 'data/source/medium-voltage-KM.xlsx'),
path.join(process.cwd(), 'data/source/low-voltage-KM.xlsx'),
path.join(process.cwd(), 'data/source/solar-cables.xlsx'),
path.join(process.cwd(), 'data/excel/high-voltage.xlsx'),
path.join(process.cwd(), 'data/excel/medium-voltage-KM.xlsx'),
path.join(process.cwd(), 'data/excel/low-voltage-KM.xlsx'),
path.join(process.cwd(), 'data/excel/solar-cables.xlsx'),
];
type AssetMap = Record<string, string>;

File diff suppressed because it is too large Load Diff