diff --git a/generate-category-descriptions.js b/generate-category-descriptions.js index 2654b43..1512f55 100644 --- a/generate-category-descriptions.js +++ b/generate-category-descriptions.js @@ -51,17 +51,35 @@ function readListFile(filePath) { throw new Error('File is empty'); } - // Parse first line: categoryName,categoryId + // Parse first line: categoryName,categoryId,[subcategoryIds] const firstLine = lines[0]; - const [categoryName, categoryId] = firstLine.split(','); + const parts = firstLine.split(','); + + if (parts.length < 2) { + throw new Error('Invalid first line format'); + } + + const categoryName = parts[0].replace(/^"|"$/g, ''); + const categoryId = parts[1].replace(/^"|"$/g, ''); + + // Parse subcategory IDs from array notation [id1,id2,...] + let subcategoryIds = []; + if (parts.length >= 3) { + const subcatString = parts.slice(2).join(','); // Handle case where array spans multiple comma-separated values + const match = subcatString.match(/\[(.*?)\]/); + if (match && match[1]) { + subcategoryIds = match[1].split(',').map(id => id.trim()).filter(id => id); + } + } if (!categoryName || !categoryId) { throw new Error('Invalid first line format'); } return { - categoryName: categoryName.replace(/^"|"$/g, ''), // Remove quotes if present - categoryId: categoryId.replace(/^"|"$/g, ''), + categoryName: categoryName, + categoryId: categoryId, + subcategoryIds: subcategoryIds, content: content }; } catch (error) { @@ -70,11 +88,66 @@ function readListFile(filePath) { } } +// Function to build processing order based on dependencies +function buildProcessingOrder(categories) { + const categoryMap = new Map(); + const processed = new Set(); + const processingOrder = []; + + // Create a map of categoryId -> category data + categories.forEach(cat => { + categoryMap.set(cat.categoryId, cat); + }); + + // Function to check if all subcategories are processed + function canProcess(category) { + return category.subcategoryIds.every(subId => processed.has(subId)); + } + + // Keep processing until all categories are done + while (processingOrder.length < categories.length) { + const beforeLength = processingOrder.length; + + // Find categories that can be processed now + for (const category of categories) { + if (!processed.has(category.categoryId) && canProcess(category)) { + processingOrder.push(category); + processed.add(category.categoryId); + } + } + + // If no progress was made, there might be a circular dependency or missing category + if (processingOrder.length === beforeLength) { + console.error('⚠️ Unable to resolve all category dependencies'); + // Add remaining categories anyway + for (const category of categories) { + if (!processed.has(category.categoryId)) { + console.warn(` Adding ${category.categoryName} (${category.categoryId}) despite unresolved dependencies`); + processingOrder.push(category); + processed.add(category.categoryId); + } + } + break; + } + } + + return processingOrder; +} + // Function to generate SEO description using OpenAI -async function generateSEODescription(productListContent, categoryName, categoryId) { +async function generateSEODescription(productListContent, categoryName, categoryId, subcategoryDescriptions = []) { try { console.log(`🔄 Generating SEO description for category: ${categoryName} (ID: ${categoryId})`); + // Prepend subcategory information if present + let fullContent = productListContent; + if (subcategoryDescriptions.length > 0) { + const subcatInfo = 'This category has the following subcategories:\n' + + subcategoryDescriptions.map(sub => `- "${sub.name}": ${sub.description}`).join('\n') + + '\n\n'; + fullContent = subcatInfo + productListContent; + } + const response = await openai.responses.create({ model: "gpt-5.1", input: [ @@ -92,7 +165,7 @@ async function generateSEODescription(productListContent, categoryName, category "content": [ { "type": "input_text", - "text": productListContent + "text": fullContent } ] } @@ -124,15 +197,8 @@ async function generateSEODescription(productListContent, categoryName, category "verbosity": "medium" }, reasoning: { - "effort": "none", - "summary": "auto" - }, - tools: [], - store: false, - include: [ - "reasoning.encrypted_content", - "web_search_call.action.sources" - ] + "effort": "none" + } }); const description = response.output_text; @@ -190,31 +256,97 @@ async function main() { console.log(`📂 Found ${listFiles.length} list files to process`); - const results = []; + // Step 1: Read all list files and extract category information + console.log('📖 Reading all category files...'); + const categories = []; + const fileDataMap = new Map(); // Map categoryId -> fileData - // Process each list file for (const listFile of listFiles) { const filePath = path.join(DIST_DIR, listFile); - - // Read and parse the file const fileData = readListFile(filePath); + if (!fileData) { console.log(`⚠️ Skipping ${listFile} due to read error`); continue; } + categories.push({ + categoryId: fileData.categoryId, + categoryName: fileData.categoryName, + subcategoryIds: fileData.subcategoryIds, + listFileName: listFile + }); + + fileDataMap.set(fileData.categoryId, { + ...fileData, + listFileName: listFile + }); + } + + console.log(`✅ Read ${categories.length} categories`); + + // Step 2: Build processing order based on dependencies + console.log('🔨 Building processing order based on category hierarchy...'); + const processingOrder = buildProcessingOrder(categories); + + const leafCategories = processingOrder.filter(cat => cat.subcategoryIds.length === 0); + const parentCategories = processingOrder.filter(cat => cat.subcategoryIds.length > 0); + + console.log(` 📄 ${leafCategories.length} leaf categories (no subcategories)`); + console.log(` 📁 ${parentCategories.length} parent categories (with subcategories)`); + + // Step 3: Process categories in order + const results = []; + const generatedDescriptions = new Map(); // Map categoryId -> {seo_description, long_description} + + for (const category of processingOrder) { + const fileData = fileDataMap.get(category.categoryId); + + if (!fileData) { + console.log(`⚠️ Skipping ${category.categoryName} - no file data found`); + continue; + } + + // Gather subcategory descriptions + const subcategoryDescriptions = []; + for (const subId of category.subcategoryIds) { + const subDesc = generatedDescriptions.get(subId); + const subCategory = categories.find(cat => cat.categoryId === subId); + + if (subDesc && subCategory) { + subcategoryDescriptions.push({ + name: subCategory.categoryName, + description: subDesc.long_description || subDesc.seo_description + }); + } else if (subCategory) { + console.warn(` ⚠️ Subcategory ${subCategory.categoryName} (${subId}) not yet processed`); + } + } + // Generate SEO description - const description = await generateSEODescription( + const descriptionJSON = await generateSEODescription( fileData.content, fileData.categoryName, - fileData.categoryId + fileData.categoryId, + subcategoryDescriptions ); + // Parse the JSON response + let parsedDescription; + try { + parsedDescription = JSON.parse(descriptionJSON); + generatedDescriptions.set(category.categoryId, parsedDescription); + } catch (error) { + console.error(` ❌ Failed to parse JSON for ${category.categoryName}:`, error.message); + parsedDescription = { seo_description: descriptionJSON, long_description: descriptionJSON }; + generatedDescriptions.set(category.categoryId, parsedDescription); + } + // Store result results.push({ - categoryId: fileData.categoryId, - listFileName: listFile, - description: description + categoryId: category.categoryId, + listFileName: fileData.listFileName, + description: parsedDescription.seo_description || descriptionJSON }); // Add delay to avoid rate limiting @@ -242,6 +374,7 @@ if (import.meta.url === `file://${process.argv[1]}`) { export { findListFiles, readListFile, + buildProcessingOrder, generateSEODescription, writeCSV }; diff --git a/prerender/renderer.cjs b/prerender/renderer.cjs index 9d371da..8e3180e 100644 --- a/prerender/renderer.cjs +++ b/prerender/renderer.cjs @@ -247,10 +247,6 @@ const renderPage = ( if (!suppressLogs) { console.log(`✅ ${description} prerendered to ${outputPath}`); console.log(` - Markup length: ${renderedMarkup.length} characters`); - console.log(` - CSS rules: ${Object.keys(cache.inserted).length}`); - console.log(` - Total inlined CSS: ${Math.round(combinedCss.length / 1024)}KB`); - console.log(` - Render-blocking CSS eliminated: ${inlinedCss ? 'YES' : 'NO'}`); - console.log(` - Fallback content saved to window.__PRERENDER_FALLBACK__`); if (productDetailCacheScript) { console.log(` - Product detail cache populated for SPA hydration`); } diff --git a/prerender/seo/feeds.cjs b/prerender/seo/feeds.cjs index de269f5..55461b4 100644 --- a/prerender/seo/feeds.cjs +++ b/prerender/seo/feeds.cjs @@ -122,6 +122,7 @@ const generateProductsXml = (allProductsData = [], baseUrl, config) => { 689: "543561", // Seeds (Saatgut) 706: "543561", // Stecklinge (cuttings) – ebenfalls Pflanzen/Saatgut 376: "2802", // Grow-Sets – Pflanzen- & Kräuteranbausets + 915: "2802", // Grow-Sets > Set-Zubehör – Pflanzen- & Kräuteranbausets // Headshop & Accessories 709: "4082", // Headshop – Rauchzubehör @@ -129,8 +130,13 @@ const generateProductsXml = (allProductsData = [], baseUrl, config) => { 714: "4082", // Headshop > Bongs > Zubehör – Rauchzubehör 748: "4082", // Headshop > Bongs > Köpfe – Rauchzubehör 749: "4082", // Headshop > Bongs > Chillums/Diffusoren/Kupplungen – Rauchzubehör + 921: "4082", // Headshop > Pfeifen – Rauchzubehör + 924: "4082", // Headshop > Dabbing – Rauchzubehör 896: "3151", // Headshop > Vaporizer – Vaporizer + 923: "4082", // Headshop > Papes & Blunts – Rauchzubehör 710: "5109", // Headshop > Grinder – Gewürzmühlen (Küchenhelfer) + 922: "4082", // Headshop > Aktivkohlefilter & Tips – Rauchzubehör + 916: "4082", // Headshop > Rollen & Bauen – Rauchzubehör // Measuring & Packaging 186: "5631", // Headshop > Wiegen & Verpacken – Aufbewahrung/Zubehör @@ -140,6 +146,7 @@ const generateProductsXml = (allProductsData = [], baseUrl, config) => { 407: "3561", // Headshop > Grove Bags – Aufbewahrungsbehälter 449: "1496", // Headshop > Cliptütchen – Lebensmittelverpackungsmaterial 539: "3110", // Headshop > Gläser & Dosen – Lebensmittelbehälter + 920: "581", // Headshop > Räucherstäbchen – Raumdüfte (Home Fragrances) // Lighting & Equipment 694: "3006", // Lampen – Lampen (Beleuchtung) diff --git a/prerender/seo/llms.cjs b/prerender/seo/llms.cjs index 8cffaf9..745766c 100644 --- a/prerender/seo/llms.cjs +++ b/prerender/seo/llms.cjs @@ -259,7 +259,8 @@ const generateCategoryProductList = (category, categoryProducts = []) => { const categorySlug = category.seoName.toLowerCase().replace(/[^a-z0-9]/g, '-'); const fileName = `llms-${categorySlug}-list.txt`; - let content = `${String(category.name)},${String(category.id)}\n`; + const subcategoryIds = (category.subcategories || []).join(','); + let content = `${String(category.name)},${String(category.id)},[${subcategoryIds}]\n`; categoryProducts.forEach((product) => { const artnr = String(product.articleNumber || ''); diff --git a/prerender/utils.cjs b/prerender/utils.cjs index ab49d3c..76f8a9b 100644 --- a/prerender/utils.cjs +++ b/prerender/utils.cjs @@ -7,11 +7,17 @@ const collectAllCategories = (categoryNode, categories = []) => { // Add current category (skip root category 209) if (categoryNode.id !== 209) { + // Extract subcategory IDs from children + const subcategoryIds = categoryNode.children + ? categoryNode.children.map(child => child.id) + : []; + categories.push({ id: categoryNode.id, name: categoryNode.name, seoName: categoryNode.seoName, - parentId: categoryNode.parentId + parentId: categoryNode.parentId, + subcategories: subcategoryIds }); }