u
This commit is contained in:
247
generate-category-descriptions.js
Normal file
247
generate-category-descriptions.js
Normal file
@@ -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
|
||||
};
|
||||
9
out
Normal file
9
out
Normal file
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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) => {
|
||||
<meta name="keywords" content="${keywords}">
|
||||
|
||||
<!-- Open Graph Meta Tags -->
|
||||
<meta property="og:title" content="${config.descriptions.short}">
|
||||
<meta property="og:title" content="${config.descriptions.de.short}">
|
||||
<meta property="og:description" content="${description}">
|
||||
<meta property="og:image" content="${imageUrl}">
|
||||
<meta property="og:url" content="${canonicalUrl}">
|
||||
@@ -21,7 +21,7 @@ const generateHomepageMetaTags = (baseUrl, config) => {
|
||||
|
||||
<!-- Twitter Card Meta Tags -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="${config.descriptions.short}">
|
||||
<meta name="twitter:title" content="${config.descriptions.de.short}">
|
||||
<meta name="twitter:description" content="${description}">
|
||||
<meta name="twitter:image" content="${imageUrl}">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
83
process_llms_cat.cjs
Normal file
83
process_llms_cat.cjs
Normal file
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user