u
This commit is contained in:
@@ -51,17 +51,35 @@ function readListFile(filePath) {
|
|||||||
throw new Error('File is empty');
|
throw new Error('File is empty');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse first line: categoryName,categoryId
|
// Parse first line: categoryName,categoryId,[subcategoryIds]
|
||||||
const firstLine = lines[0];
|
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) {
|
if (!categoryName || !categoryId) {
|
||||||
throw new Error('Invalid first line format');
|
throw new Error('Invalid first line format');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
categoryName: categoryName.replace(/^"|"$/g, ''), // Remove quotes if present
|
categoryName: categoryName,
|
||||||
categoryId: categoryId.replace(/^"|"$/g, ''),
|
categoryId: categoryId,
|
||||||
|
subcategoryIds: subcategoryIds,
|
||||||
content: content
|
content: content
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} 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
|
// Function to generate SEO description using OpenAI
|
||||||
async function generateSEODescription(productListContent, categoryName, categoryId) {
|
async function generateSEODescription(productListContent, categoryName, categoryId, subcategoryDescriptions = []) {
|
||||||
try {
|
try {
|
||||||
console.log(`🔄 Generating SEO description for category: ${categoryName} (ID: ${categoryId})`);
|
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({
|
const response = await openai.responses.create({
|
||||||
model: "gpt-5.1",
|
model: "gpt-5.1",
|
||||||
input: [
|
input: [
|
||||||
@@ -92,7 +165,7 @@ async function generateSEODescription(productListContent, categoryName, category
|
|||||||
"content": [
|
"content": [
|
||||||
{
|
{
|
||||||
"type": "input_text",
|
"type": "input_text",
|
||||||
"text": productListContent
|
"text": fullContent
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -124,15 +197,8 @@ async function generateSEODescription(productListContent, categoryName, category
|
|||||||
"verbosity": "medium"
|
"verbosity": "medium"
|
||||||
},
|
},
|
||||||
reasoning: {
|
reasoning: {
|
||||||
"effort": "none",
|
"effort": "none"
|
||||||
"summary": "auto"
|
}
|
||||||
},
|
|
||||||
tools: [],
|
|
||||||
store: false,
|
|
||||||
include: [
|
|
||||||
"reasoning.encrypted_content",
|
|
||||||
"web_search_call.action.sources"
|
|
||||||
]
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const description = response.output_text;
|
const description = response.output_text;
|
||||||
@@ -190,31 +256,97 @@ async function main() {
|
|||||||
|
|
||||||
console.log(`📂 Found ${listFiles.length} list files to process`);
|
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) {
|
for (const listFile of listFiles) {
|
||||||
const filePath = path.join(DIST_DIR, listFile);
|
const filePath = path.join(DIST_DIR, listFile);
|
||||||
|
|
||||||
// Read and parse the file
|
|
||||||
const fileData = readListFile(filePath);
|
const fileData = readListFile(filePath);
|
||||||
|
|
||||||
if (!fileData) {
|
if (!fileData) {
|
||||||
console.log(`⚠️ Skipping ${listFile} due to read error`);
|
console.log(`⚠️ Skipping ${listFile} due to read error`);
|
||||||
continue;
|
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
|
// Generate SEO description
|
||||||
const description = await generateSEODescription(
|
const descriptionJSON = await generateSEODescription(
|
||||||
fileData.content,
|
fileData.content,
|
||||||
fileData.categoryName,
|
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
|
// Store result
|
||||||
results.push({
|
results.push({
|
||||||
categoryId: fileData.categoryId,
|
categoryId: category.categoryId,
|
||||||
listFileName: listFile,
|
listFileName: fileData.listFileName,
|
||||||
description: description
|
description: parsedDescription.seo_description || descriptionJSON
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add delay to avoid rate limiting
|
// Add delay to avoid rate limiting
|
||||||
@@ -242,6 +374,7 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
|||||||
export {
|
export {
|
||||||
findListFiles,
|
findListFiles,
|
||||||
readListFile,
|
readListFile,
|
||||||
|
buildProcessingOrder,
|
||||||
generateSEODescription,
|
generateSEODescription,
|
||||||
writeCSV
|
writeCSV
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -247,10 +247,6 @@ const renderPage = (
|
|||||||
if (!suppressLogs) {
|
if (!suppressLogs) {
|
||||||
console.log(`✅ ${description} prerendered to ${outputPath}`);
|
console.log(`✅ ${description} prerendered to ${outputPath}`);
|
||||||
console.log(` - Markup length: ${renderedMarkup.length} characters`);
|
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) {
|
if (productDetailCacheScript) {
|
||||||
console.log(` - Product detail cache populated for SPA hydration`);
|
console.log(` - Product detail cache populated for SPA hydration`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ const generateProductsXml = (allProductsData = [], baseUrl, config) => {
|
|||||||
689: "543561", // Seeds (Saatgut)
|
689: "543561", // Seeds (Saatgut)
|
||||||
706: "543561", // Stecklinge (cuttings) – ebenfalls Pflanzen/Saatgut
|
706: "543561", // Stecklinge (cuttings) – ebenfalls Pflanzen/Saatgut
|
||||||
376: "2802", // Grow-Sets – Pflanzen- & Kräuteranbausets
|
376: "2802", // Grow-Sets – Pflanzen- & Kräuteranbausets
|
||||||
|
915: "2802", // Grow-Sets > Set-Zubehör – Pflanzen- & Kräuteranbausets
|
||||||
|
|
||||||
// Headshop & Accessories
|
// Headshop & Accessories
|
||||||
709: "4082", // Headshop – Rauchzubehör
|
709: "4082", // Headshop – Rauchzubehör
|
||||||
@@ -129,8 +130,13 @@ const generateProductsXml = (allProductsData = [], baseUrl, config) => {
|
|||||||
714: "4082", // Headshop > Bongs > Zubehör – Rauchzubehör
|
714: "4082", // Headshop > Bongs > Zubehör – Rauchzubehör
|
||||||
748: "4082", // Headshop > Bongs > Köpfe – Rauchzubehör
|
748: "4082", // Headshop > Bongs > Köpfe – Rauchzubehör
|
||||||
749: "4082", // Headshop > Bongs > Chillums/Diffusoren/Kupplungen – 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
|
896: "3151", // Headshop > Vaporizer – Vaporizer
|
||||||
|
923: "4082", // Headshop > Papes & Blunts – Rauchzubehör
|
||||||
710: "5109", // Headshop > Grinder – Gewürzmühlen (Küchenhelfer)
|
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
|
// Measuring & Packaging
|
||||||
186: "5631", // Headshop > Wiegen & Verpacken – Aufbewahrung/Zubehör
|
186: "5631", // Headshop > Wiegen & Verpacken – Aufbewahrung/Zubehör
|
||||||
@@ -140,6 +146,7 @@ const generateProductsXml = (allProductsData = [], baseUrl, config) => {
|
|||||||
407: "3561", // Headshop > Grove Bags – Aufbewahrungsbehälter
|
407: "3561", // Headshop > Grove Bags – Aufbewahrungsbehälter
|
||||||
449: "1496", // Headshop > Cliptütchen – Lebensmittelverpackungsmaterial
|
449: "1496", // Headshop > Cliptütchen – Lebensmittelverpackungsmaterial
|
||||||
539: "3110", // Headshop > Gläser & Dosen – Lebensmittelbehälter
|
539: "3110", // Headshop > Gläser & Dosen – Lebensmittelbehälter
|
||||||
|
920: "581", // Headshop > Räucherstäbchen – Raumdüfte (Home Fragrances)
|
||||||
|
|
||||||
// Lighting & Equipment
|
// Lighting & Equipment
|
||||||
694: "3006", // Lampen – Lampen (Beleuchtung)
|
694: "3006", // Lampen – Lampen (Beleuchtung)
|
||||||
|
|||||||
@@ -259,7 +259,8 @@ const generateCategoryProductList = (category, categoryProducts = []) => {
|
|||||||
const categorySlug = category.seoName.toLowerCase().replace(/[^a-z0-9]/g, '-');
|
const categorySlug = category.seoName.toLowerCase().replace(/[^a-z0-9]/g, '-');
|
||||||
const fileName = `llms-${categorySlug}-list.txt`;
|
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) => {
|
categoryProducts.forEach((product) => {
|
||||||
const artnr = String(product.articleNumber || '');
|
const artnr = String(product.articleNumber || '');
|
||||||
|
|||||||
@@ -7,11 +7,17 @@ const collectAllCategories = (categoryNode, categories = []) => {
|
|||||||
|
|
||||||
// Add current category (skip root category 209)
|
// Add current category (skip root category 209)
|
||||||
if (categoryNode.id !== 209) {
|
if (categoryNode.id !== 209) {
|
||||||
|
// Extract subcategory IDs from children
|
||||||
|
const subcategoryIds = categoryNode.children
|
||||||
|
? categoryNode.children.map(child => child.id)
|
||||||
|
: [];
|
||||||
|
|
||||||
categories.push({
|
categories.push({
|
||||||
id: categoryNode.id,
|
id: categoryNode.id,
|
||||||
name: categoryNode.name,
|
name: categoryNode.name,
|
||||||
seoName: categoryNode.seoName,
|
seoName: categoryNode.seoName,
|
||||||
parentId: categoryNode.parentId
|
parentId: categoryNode.parentId,
|
||||||
|
subcategories: subcategoryIds
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user