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); +}