diff --git a/generate-category-descriptions.js b/generate-category-descriptions.js
new file mode 100644
index 0000000..2654b43
--- /dev/null
+++ b/generate-category-descriptions.js
@@ -0,0 +1,247 @@
+#!/usr/bin/env node
+
+import fs from 'fs';
+import path from 'path';
+import OpenAI from 'openai';
+
+// Configuration
+const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
+const DIST_DIR = './dist';
+const OUTPUT_CSV = './category-descriptions.csv';
+
+// Model configuration
+const MODEL = 'gpt-5.1';
+
+// Initialize OpenAI client
+const openai = new OpenAI({
+ apiKey: OPENAI_API_KEY,
+});
+
+// System prompt for generating SEO descriptions
+const SEO_DESCRIPTION_PROMPT = `You are given a list of products from a specific category. Create a SEO-friendly description for that category that would be suitable for a product catalog page.
+
+Requirements:
+- Write in German
+- Make it SEO-optimized with relevant keywords
+
+The product list format is:
+First line: categoryName,categoryId
+Subsequent lines: articleNumber,price,productName,shortDescription
+
+Generate a compelling category description based on this product data.`;
+
+// Function to find all *-list.txt files in dist directory
+function findListFiles() {
+ try {
+ const files = fs.readdirSync(DIST_DIR);
+ return files.filter(file => file.endsWith('-list.txt'));
+ } catch (error) {
+ console.error('Error reading dist directory:', error.message);
+ return [];
+ }
+}
+
+// Function to read a list file and extract category info
+function readListFile(filePath) {
+ try {
+ const content = fs.readFileSync(filePath, 'utf8');
+ const lines = content.trim().split('\n');
+
+ if (lines.length < 1) {
+ throw new Error('File is empty');
+ }
+
+ // Parse first line: categoryName,categoryId
+ const firstLine = lines[0];
+ const [categoryName, categoryId] = firstLine.split(',');
+
+ if (!categoryName || !categoryId) {
+ throw new Error('Invalid first line format');
+ }
+
+ return {
+ categoryName: categoryName.replace(/^"|"$/g, ''), // Remove quotes if present
+ categoryId: categoryId.replace(/^"|"$/g, ''),
+ content: content
+ };
+ } catch (error) {
+ console.error(`Error reading ${filePath}:`, error.message);
+ return null;
+ }
+}
+
+// Function to generate SEO description using OpenAI
+async function generateSEODescription(productListContent, categoryName, categoryId) {
+ try {
+ console.log(`🔄 Generating SEO description for category: ${categoryName} (ID: ${categoryId})`);
+
+ const response = await openai.responses.create({
+ model: "gpt-5.1",
+ input: [
+ {
+ "role": "developer",
+ "content": [
+ {
+ "type": "input_text",
+ "text": SEO_DESCRIPTION_PROMPT
+ }
+ ]
+ },
+ {
+ "role": "user",
+ "content": [
+ {
+ "type": "input_text",
+ "text": productListContent
+ }
+ ]
+ }
+ ],
+ text: {
+ "format": {
+ "type": "json_schema",
+ "name": "descriptions",
+ "strict": true,
+ "schema": {
+ "type": "object",
+ "properties": {
+ "seo_description": {
+ "type": "string",
+ "description": "A concise description intended for SEO purposes. 155 characters"
+ },
+ "long_description": {
+ "type": "string",
+ "description": "A comprehensive description, 2-5 Sentences"
+ }
+ },
+ "required": [
+ "seo_description",
+ "long_description"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "verbosity": "medium"
+ },
+ reasoning: {
+ "effort": "none",
+ "summary": "auto"
+ },
+ tools: [],
+ store: false,
+ include: [
+ "reasoning.encrypted_content",
+ "web_search_call.action.sources"
+ ]
+ });
+
+ const description = response.output_text;
+ console.log(`✅ Generated description for ${categoryName}`);
+ return description;
+
+ } catch (error) {
+ console.error(`❌ Error generating description for ${categoryName}:`, error.message);
+ return `Error generating description: ${error.message}`;
+ }
+}
+
+// Function to write CSV file
+function writeCSV(results) {
+ try {
+ const csvHeader = 'categoryId,listFileName,seoDescription\n';
+ const csvRows = results.map(result =>
+ `"${result.categoryId}","${result.listFileName}","${result.description.replace(/"/g, '""')}"`
+ ).join('\n');
+
+ const csvContent = csvHeader + csvRows;
+ fs.writeFileSync(OUTPUT_CSV, csvContent, 'utf8');
+ console.log(`✅ CSV file written: ${OUTPUT_CSV}`);
+ console.log(`📊 Processed ${results.length} categories`);
+
+ } catch (error) {
+ console.error('Error writing CSV file:', error.message);
+ }
+}
+
+// Main execution function
+async function main() {
+ console.log('🚀 Starting category description generation...');
+
+ // Check if OpenAI API key is set
+ if (!OPENAI_API_KEY) {
+ console.error('❌ OPENAI_API_KEY environment variable is not set');
+ console.log('Please set your OpenAI API key: export OPENAI_API_KEY="your-api-key-here"');
+ process.exit(1);
+ }
+
+ // Check if dist directory exists
+ if (!fs.existsSync(DIST_DIR)) {
+ console.error(`❌ Dist directory not found: ${DIST_DIR}`);
+ process.exit(1);
+ }
+
+ // Find all list files
+ const listFiles = findListFiles();
+ if (listFiles.length === 0) {
+ console.log('⚠️ No *-list.txt files found in dist directory');
+ console.log('💡 Make sure to run the prerender script first to generate the list files');
+ process.exit(1);
+ }
+
+ console.log(`📂 Found ${listFiles.length} list files to process`);
+
+ const results = [];
+
+ // 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;
+ }
+
+ // Generate SEO description
+ const description = await generateSEODescription(
+ fileData.content,
+ fileData.categoryName,
+ fileData.categoryId
+ );
+
+ // Store result
+ results.push({
+ categoryId: fileData.categoryId,
+ listFileName: listFile,
+ description: description
+ });
+
+ // Add delay to avoid rate limiting
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ }
+
+ // Write CSV output
+ if (results.length > 0) {
+ writeCSV(results);
+ console.log('🎉 Category description generation completed successfully!');
+ } else {
+ console.error('❌ No results to write - all files failed processing');
+ process.exit(1);
+ }
+}
+
+// Run the script
+if (import.meta.url === `file://${process.argv[1]}`) {
+ main().catch(error => {
+ console.error('❌ Script failed:', error.message);
+ process.exit(1);
+ });
+}
+
+export {
+ findListFiles,
+ readListFile,
+ generateSEODescription,
+ writeCSV
+};
diff --git a/out b/out
new file mode 100644
index 0000000..24d3267
--- /dev/null
+++ b/out
@@ -0,0 +1,9 @@
+
+> reactshop@1.0.0 prerender:prod
+> cross-env NODE_ENV=production node prerender.cjs
+
+🔧 Prerender mode: PRODUCTION
+📁 Output directory: dist
+📦 Found webpack entrypoints:
+ JS files: 7 files
+ CSS files: 0 files
diff --git a/prerender.cjs b/prerender.cjs
index 45ae1da..7faecd2 100644
--- a/prerender.cjs
+++ b/prerender.cjs
@@ -136,6 +136,7 @@ const {
generateLlmsTxt,
generateCategoryLlmsTxt,
generateAllCategoryLlmsPages,
+ generateCategoryProductList,
} = require("./prerender/seo.cjs");
const {
fetchCategoryProducts,
@@ -794,11 +795,17 @@ const renderApp = async (categoryData, socket) => {
fs.writeFileSync(pagePath, page.content, { encoding: 'utf8' });
totalPaginatedFiles++;
}
-
+
+ // Generate and write the product list file for this category
+ const productList = generateCategoryProductList(category, categoryProducts);
+ const listPath = path.resolve(__dirname, config.outputDir, productList.fileName);
+ fs.writeFileSync(listPath, productList.content, { encoding: 'utf8' });
+
const pageCount = categoryPages.length;
const totalSize = categoryPages.reduce((sum, page) => sum + page.content.length, 0);
-
+
console.log(` ✅ llms-${categorySlug}-page-*.txt - ${categoryProducts.length} products across ${pageCount} pages (${Math.round(totalSize / 1024)}KB total)`);
+ console.log(` 📋 ${productList.fileName} - ${productList.productCount} products (${Math.round(productList.content.length / 1024)}KB)`);
categoryFilesGenerated++;
totalCategoryProducts += categoryProducts.length;
diff --git a/prerender/seo/homepage.cjs b/prerender/seo/homepage.cjs
index a560c68..891df79 100644
--- a/prerender/seo/homepage.cjs
+++ b/prerender/seo/homepage.cjs
@@ -1,6 +1,6 @@
const generateHomepageMetaTags = (baseUrl, config) => {
- const description = config.descriptions.long;
- const keywords = config.keywords;
+ const description = config.descriptions.de.long;
+ const keywords = config.keywords.de;
const imageUrl = `${baseUrl}${config.images.logo}`;
// Ensure URLs are properly formatted
@@ -12,7 +12,7 @@ const generateHomepageMetaTags = (baseUrl, config) => {
-
+
@@ -21,7 +21,7 @@ const generateHomepageMetaTags = (baseUrl, config) => {
-
+
@@ -41,7 +41,7 @@ const generateHomepageJsonLd = (baseUrl, config, categories = []) => {
"@type": "WebSite",
name: config.brandName,
url: canonicalUrl,
- description: config.descriptions.long,
+ description: config.descriptions.de.long,
publisher: {
"@type": "Organization",
name: config.brandName,
@@ -73,7 +73,7 @@ const generateHomepageJsonLd = (baseUrl, config, categories = []) => {
"@type": "LocalBusiness",
"name": config.brandName,
"alternateName": config.siteName,
- "description": config.descriptions.long,
+ "description": config.descriptions.de.long,
"url": canonicalUrl,
"logo": logoUrl,
"image": logoUrl,
diff --git a/prerender/seo/index.cjs b/prerender/seo/index.cjs
index ee08850..f660412 100644
--- a/prerender/seo/index.cjs
+++ b/prerender/seo/index.cjs
@@ -31,6 +31,7 @@ const {
generateLlmsTxt,
generateCategoryLlmsTxt,
generateAllCategoryLlmsPages,
+ generateCategoryProductList,
} = require('./llms.cjs');
// Export all functions for use in the main application
@@ -61,4 +62,5 @@ module.exports = {
generateLlmsTxt,
generateCategoryLlmsTxt,
generateAllCategoryLlmsPages,
+ generateCategoryProductList,
};
\ No newline at end of file
diff --git a/prerender/seo/llms.cjs b/prerender/seo/llms.cjs
index f8171ea..8cffaf9 100644
--- a/prerender/seo/llms.cjs
+++ b/prerender/seo/llms.cjs
@@ -254,17 +254,51 @@ This category currently contains no products.
return categoryLlmsTxt;
};
+// Helper function to generate a simple product list for a category
+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`;
+
+ categoryProducts.forEach((product) => {
+ const artnr = String(product.articleNumber || '');
+ const price = String(product.price || '0.00');
+ const name = String(product.name || '');
+ const kurzBeschreibung = String(product.kurzBeschreibung || '');
+
+ // Escape commas in fields by wrapping in quotes if they contain commas
+ const escapeField = (field) => {
+ const fieldStr = String(field || '');
+ if (fieldStr.includes(',')) {
+ return `"${fieldStr.replace(/"/g, '""')}"`;
+ }
+ return fieldStr;
+ };
+
+ content += `${escapeField(artnr)},${escapeField(price)},${escapeField(name)},${escapeField(kurzBeschreibung)}\n`;
+ });
+
+ return {
+ fileName,
+ content,
+ categoryName: category.name,
+ categoryId: category.id,
+ productCount: categoryProducts.length
+ };
+};
+
// Helper function to generate all pages for a category
const generateAllCategoryLlmsPages = (category, categoryProducts = [], baseUrl, config, productsPerPage = 50) => {
const totalProducts = categoryProducts.length;
const totalPages = Math.ceil(totalProducts / productsPerPage);
const pages = [];
-
+
for (let pageNumber = 1; pageNumber <= totalPages; pageNumber++) {
const pageContent = generateCategoryLlmsTxt(category, categoryProducts, baseUrl, config, pageNumber, productsPerPage);
const categorySlug = category.seoName.toLowerCase().replace(/[^a-z0-9]/g, '-');
const fileName = `llms-${categorySlug}-page-${pageNumber}.txt`;
-
+
pages.push({
fileName,
content: pageContent,
@@ -272,7 +306,7 @@ const generateAllCategoryLlmsPages = (category, categoryProducts = [], baseUrl,
totalPages
});
}
-
+
return pages;
};
@@ -280,4 +314,5 @@ module.exports = {
generateLlmsTxt,
generateCategoryLlmsTxt,
generateAllCategoryLlmsPages,
+ generateCategoryProductList,
};
\ No newline at end of file
diff --git a/process_llms_cat.cjs b/process_llms_cat.cjs
new file mode 100644
index 0000000..2ac9105
--- /dev/null
+++ b/process_llms_cat.cjs
@@ -0,0 +1,83 @@
+const fs = require('fs');
+const path = require('path');
+
+// Read the input file
+const inputFile = path.join(__dirname, 'dist', 'llms-cat.txt');
+const outputFile = path.join(__dirname, 'output.csv');
+
+// Function to parse a CSV line with escaped quotes
+function parseCSVLine(line) {
+ const fields = [];
+ let current = '';
+ let inQuotes = false;
+ let i = 0;
+
+ while (i < line.length) {
+ const char = line[i];
+
+ if (char === '"') {
+ // Check if this is an escaped quote
+ if (i + 1 < line.length && line[i + 1] === '"') {
+ current += '"'; // Add single quote (unescaped)
+ i += 2; // Skip both quotes
+ continue;
+ } else {
+ inQuotes = !inQuotes; // Toggle quote state
+ }
+ } else if (char === ',' && !inQuotes) {
+ fields.push(current);
+ current = '';
+ } else {
+ current += char;
+ }
+ i++;
+ }
+
+ fields.push(current); // Add the last field
+ return fields;
+}
+
+try {
+ const data = fs.readFileSync(inputFile, 'utf8');
+ const lines = data.trim().split('\n');
+
+ const outputLines = ['URL,SEO Description'];
+
+ for (const line of lines) {
+ if (line.trim() === '') continue;
+
+ // Parse the CSV line properly handling escaped quotes
+ const fields = parseCSVLine(line);
+
+ if (fields.length !== 3) {
+ console.warn(`Skipping malformed line (got ${fields.length} fields): ${line.substring(0, 100)}...`);
+ continue;
+ }
+
+ const [field1, field2, field3] = fields;
+ const url = field2;
+
+ // field3 is a JSON string - parse it directly
+ let seoDescription = '';
+ try {
+ const parsed = JSON.parse(field3);
+ seoDescription = parsed.seo_description || '';
+ } catch (e) {
+ console.warn(`Failed to parse JSON for URL ${url}: ${e.message}`);
+ console.warn(`JSON string: ${field3.substring(0, 200)}...`);
+ }
+
+ // Escape quotes for CSV output - URL doesn't need quotes, description does
+ const escapedDescription = '"' + seoDescription.replace(/"/g, '""') + '"';
+
+ outputLines.push(`${url},${escapedDescription}`);
+ }
+
+ // Write the output CSV
+ fs.writeFileSync(outputFile, outputLines.join('\n'), 'utf8');
+ console.log(`Processed ${lines.length} lines and created ${outputFile}`);
+
+} catch (error) {
+ console.error('Error processing file:', error.message);
+ process.exit(1);
+}