diff --git a/prerender/seo.cjs b/prerender/seo.cjs
index ecfb57a..78347e2 100644
--- a/prerender/seo.cjs
+++ b/prerender/seo.cjs
@@ -1,1193 +1,4 @@
-const generateProductMetaTags = (product, baseUrl, config) => {
- const productUrl = `${baseUrl}/Artikel/${product.seoName}`;
- const imageUrl =
- product.pictureList && product.pictureList.trim()
- ? `${baseUrl}/assets/images/prod${product.pictureList
- .split(",")[0]
- .trim()}.jpg`
- : `${baseUrl}/assets/images/nopicture.jpg`;
+// Re-export all SEO functions from the new modular structure
+// This maintains backward compatibility while using the new split files
- // Clean description for meta (remove HTML tags and limit length)
- const cleanDescription = product.description
- ? product.description
- .replace(/<[^>]*>/g, "")
- .replace(/\n/g, " ")
- .substring(0, 160)
- : `${product.name} - Art.-Nr.: ${product.articleNumber}`;
-
- return `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ${product.gtin ? `` : ''}
- ${product.articleNumber ? `` : ''}
- ${product.manufacturer ? `` : ''}
-
-
-
-
-
-
-
-
-
-
- `;
-};
-
-const generateProductJsonLd = (product, baseUrl, config, categoryInfo = null) => {
- const productUrl = `${baseUrl}/Artikel/${product.seoName}`;
- const imageUrl =
- product.pictureList && product.pictureList.trim()
- ? `${baseUrl}/assets/images/prod${product.pictureList
- .split(",")[0]
- .trim()}.jpg`
- : `${baseUrl}/assets/images/nopicture.jpg`;
-
- // Clean description for JSON-LD (remove HTML tags)
- const cleanDescription = product.description
- ? product.description.replace(/<[^>]*>/g, "").replace(/\n/g, " ")
- : product.name;
-
- // Calculate price valid date (current date + 3 months)
- const priceValidDate = new Date();
- priceValidDate.setMonth(priceValidDate.getMonth() + 3);
-
- const jsonLd = {
- "@context": "https://schema.org/",
- "@type": "Product",
- name: product.name,
- image: [imageUrl],
- description: cleanDescription,
- sku: product.articleNumber,
- ...(product.gtin && { gtin: product.gtin }),
- brand: {
- "@type": "Brand",
- name: product.manufacturer || "Unknown",
- },
- offers: {
- "@type": "Offer",
- url: productUrl,
- priceCurrency: config.currency,
- price: product.price.toString(),
- priceValidUntil: priceValidDate.toISOString().split("T")[0],
- itemCondition: "https://schema.org/NewCondition",
- availability: product.available
- ? "https://schema.org/InStock"
- : "https://schema.org/OutOfStock",
- seller: {
- "@type": "Organization",
- name: config.brandName,
- },
- },
- };
-
- // Add breadcrumb if category information is available
- if (categoryInfo && categoryInfo.name && categoryInfo.seoName) {
- jsonLd.breadcrumb = {
- "@type": "BreadcrumbList",
- itemListElement: [
- {
- "@type": "ListItem",
- position: 1,
- name: "Home",
- item: baseUrl,
- },
- {
- "@type": "ListItem",
- position: 2,
- name: categoryInfo.name,
- item: `${baseUrl}/Kategorie/${categoryInfo.seoName}`,
- },
- {
- "@type": "ListItem",
- position: 3,
- name: product.name,
- item: productUrl,
- },
- ],
- };
- }
-
- return ``;
-};
-
-const generateCategoryJsonLd = (category, products = [], baseUrl, config) => {
- const categoryUrl = `${baseUrl}/Kategorie/${category.seoName}`;
-
- const jsonLd = {
- "@context": "https://schema.org/",
- "@type": "CollectionPage",
- name: category.name,
- url: categoryUrl,
- description: `${category.name} - Entdecken Sie unsere Auswahl an hochwertigen Produkten`,
- breadcrumb: {
- "@type": "BreadcrumbList",
- itemListElement: [
- {
- "@type": "ListItem",
- position: 1,
- name: "Home",
- item: baseUrl,
- },
- {
- "@type": "ListItem",
- position: 2,
- name: category.name,
- item: categoryUrl,
- },
- ],
- },
- };
-
- // Add product list if products are available
- if (products && products.length > 0) {
- jsonLd.mainEntity = {
- "@type": "ItemList",
- numberOfItems: products.length,
- itemListElement: products.slice(0, 20).map((product, index) => ({
- "@type": "ListItem",
- position: index + 1,
- item: {
- "@type": "Product",
- name: product.name,
- url: `${baseUrl}/Artikel/${product.seoName}`,
- image:
- product.pictureList && product.pictureList.trim()
- ? `${baseUrl}/assets/images/prod${product.pictureList
- .split(",")[0]
- .trim()}.jpg`
- : `${baseUrl}/assets/images/nopicture.jpg`,
- description: product.description
- ? product.description.replace(/<[^>]*>/g, "").substring(0, 200)
- : `${product.name} - Hochwertiges Growshop Produkt`,
- sku: product.articleNumber || product.seoName,
- brand: {
- "@type": "Brand",
- name: product.manufacturer || config.brandName,
- },
- offers: {
- "@type": "Offer",
- url: `${baseUrl}/Artikel/${product.seoName}`,
- price: product.price && !isNaN(product.price) ? product.price.toString() : "0.00",
- priceCurrency: config.currency,
- availability: product.available
- ? "https://schema.org/InStock"
- : "https://schema.org/OutOfStock",
- seller: {
- "@type": "Organization",
- name: config.brandName,
- },
- itemCondition: "https://schema.org/NewCondition",
- },
- },
- })),
- };
- }
-
- return ``;
-};
-
-const generateHomepageMetaTags = (baseUrl, config) => {
- const description = config.descriptions.long;
- const keywords = config.keywords;
- const imageUrl = `${baseUrl}${config.images.logo}`;
-
- // Ensure URLs are properly formatted
- const canonicalUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
-
- return `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- `;
-};
-
-const generateHomepageJsonLd = (baseUrl, config, categories = []) => {
- // Ensure URLs are properly formatted
- const canonicalUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
- const logoUrl = `${canonicalUrl}${config.images.logo}`;
-
- const websiteJsonLd = {
- "@context": "https://schema.org/",
- "@type": "WebSite",
- name: config.brandName,
- url: canonicalUrl,
- description: config.descriptions.long,
- publisher: {
- "@type": "Organization",
- name: config.brandName,
- url: canonicalUrl,
- logo: {
- "@type": "ImageObject",
- url: logoUrl,
- },
- },
- potentialAction: {
- "@type": "SearchAction",
- target: `${canonicalUrl}/search?q={search_term_string}`,
- query: "required name=search_term_string"
- },
- mainEntity: {
- "@type": "WebPage",
- name: "Sitemap",
- url: `${canonicalUrl}/sitemap`,
- description: "Vollständige Sitemap mit allen Kategorien und Seiten",
- },
- sameAs: [
- // Add your social media URLs here if available
- ],
- };
-
- // Organization/LocalBusiness Schema for rich results
- const organizationJsonLd = {
- "@context": "https://schema.org",
- "@type": "LocalBusiness",
- "name": config.brandName,
- "alternateName": config.siteName,
- "description": config.descriptions.long,
- "url": canonicalUrl,
- "logo": logoUrl,
- "image": logoUrl,
- "telephone": "015208491860",
- "email": "service@growheads.de",
- "address": {
- "@type": "PostalAddress",
- "streetAddress": "Trachenberger Strasse 14",
- "addressLocality": "Dresden",
- "postalCode": "01129",
- "addressCountry": "DE",
- "addressRegion": "Sachsen"
- },
- "geo": {
- "@type": "GeoCoordinates",
- "latitude": "51.083675",
- "longitude": "13.727215"
- },
- "openingHours": [
- "Mo-Fr 10:00:00-20:00:00",
- "Sa 11:00:00-19:00:00"
- ],
- "paymentAccepted": "Cash, Credit Card, PayPal, Bank Transfer",
- "currenciesAccepted": "EUR",
- "priceRange": "€€",
- "areaServed": {
- "@type": "Country",
- "name": "Germany"
- },
- "contactPoint": [
- {
- "@type": "ContactPoint",
- "telephone": "015208491860",
- "contactType": "customer service",
- "availableLanguage": "German",
- "hoursAvailable": {
- "@type": "OpeningHoursSpecification",
- "dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
- "opens": "10:00:00",
- "closes": "20:00:00"
- }
- },
- {
- "@type": "ContactPoint",
- "email": "service@growheads.de",
- "contactType": "customer service",
- "availableLanguage": "German"
- }
- ],
- "sameAs": [
- // Add social media URLs when available
- // "https://www.facebook.com/growheads",
- // "https://www.instagram.com/growheads"
- ]
- };
-
- // FAQPage Schema for common questions
- const faqJsonLd = {
- "@context": "https://schema.org",
- "@type": "FAQPage",
- "mainEntity": [
- {
- "@type": "Question",
- "name": "Welche Zahlungsmethoden akzeptiert GrowHeads?",
- "acceptedAnswer": {
- "@type": "Answer",
- "text": "Wir akzeptieren Kreditkarten, PayPal, Banküberweisung, Nachnahme und Barzahlung bei Abholung in unserem Laden in Dresden."
- }
- },
- {
- "@type": "Question",
- "name": "Liefert GrowHeads deutschlandweit?",
- "acceptedAnswer": {
- "@type": "Answer",
- "text": "Ja, wir liefern deutschlandweit. Zusätzlich haben wir einen Laden in Dresden (Trachenberger Strasse 14) für lokale Kunden."
- }
- },
- {
- "@type": "Question",
- "name": "Welche Produkte bietet GrowHeads?",
- "acceptedAnswer": {
- "@type": "Answer",
- "text": "Wir bieten ein komplettes Sortiment für den Indoor-Anbau: Beleuchtung, Belüftung, Dünger, Töpfe, Zelte, Messgeräte und vieles mehr für professionelle Zuchtanlagen."
- }
- },
- {
- "@type": "Question",
- "name": "Hat GrowHeads einen physischen Laden?",
- "acceptedAnswer": {
- "@type": "Answer",
- "text": "Ja, unser Laden befindet sich in Dresden, Trachenberger Strasse 14. Öffnungszeiten: Mo-Fr 10-20 Uhr, Sa 11-19 Uhr. Sie können auch online bestellen."
- }
- },
- {
- "@type": "Question",
- "name": "Bietet GrowHeads Beratung zum Indoor-Anbau?",
- "acceptedAnswer": {
- "@type": "Answer",
- "text": "Ja, unser erfahrenes Team berät Sie gerne zu allen Aspekten des Indoor-Anbaus. Kontaktieren Sie uns telefonisch unter 015208491860 oder besuchen Sie unseren Laden."
- }
- }
- ]
- };
-
- // Generate ItemList for all categories (more appropriate for homepage)
- const categoriesListJsonLd = {
- "@context": "https://schema.org",
- "@type": "ItemList",
- "name": "Produktkategorien",
- "description": "Alle verfügbaren Produktkategorien in unserem Online-Shop",
- "numberOfItems": categories.filter(category => category.seoName).length,
- "itemListElement": categories
- .filter(category => category.seoName) // Only include categories with seoName
- .map((category, index) => ({
- "@type": "ListItem",
- "position": index + 1,
- "item": {
- "@type": "Thing",
- "name": category.name,
- "url": `${canonicalUrl}/Kategorie/${category.seoName}`
- }
- }))
- };
-
- // Return all JSON-LD scripts
- const websiteScript = ``;
- const organizationScript = ``;
- const faqScript = ``;
- const categoriesScript = categories.length > 0
- ? ``
- : '';
-
- return websiteScript + '\n' + organizationScript + '\n' + faqScript + (categoriesScript ? '\n' + categoriesScript : '');
-};
-
-const generateSitemapJsonLd = (allCategories = [], baseUrl, config) => {
-
- const jsonLd = {
- "@context": "https://schema.org/",
- "@type": "WebPage",
- name: "Sitemap",
- url: `${baseUrl}/sitemap`,
- description: `Sitemap - Übersicht aller Kategorien und Seiten auf ${config.siteName}`,
- breadcrumb: {
- "@type": "BreadcrumbList",
- itemListElement: [
- {
- "@type": "ListItem",
- position: 1,
- name: "Home",
- item: baseUrl,
- },
- {
- "@type": "ListItem",
- position: 2,
- name: "Sitemap",
- item: `${baseUrl}/sitemap`,
- },
- ],
- },
- };
-
- // Add all categories as site navigation elements
- if (allCategories && allCategories.length > 0) {
- jsonLd.mainEntity = {
- "@type": "SiteNavigationElement",
- name: "Kategorien",
- hasPart: allCategories.map((category) => ({
- "@type": "SiteNavigationElement",
- name: category.name,
- url: `${baseUrl}/Kategorie/${category.seoName}`,
- description: `${category.name} Kategorie`,
- })),
- };
- }
-
- return ``;
-};
-
-const generateXmlSitemap = (allCategories = [], allProducts = [], baseUrl) => {
- const currentDate = new Date().toISOString().split("T")[0]; // YYYY-MM-DD format
-
- let sitemap = `
-
-`;
-
- // Homepage
- sitemap += `
- ${baseUrl}/
- ${currentDate}
- daily
- 1.0
-
-`;
-
- // Static pages
- const staticPages = [
- { path: "/datenschutz", changefreq: "monthly", priority: "0.3" },
- { path: "/impressum", changefreq: "monthly", priority: "0.3" },
- { path: "/batteriegesetzhinweise", changefreq: "monthly", priority: "0.3" },
- { path: "/widerrufsrecht", changefreq: "monthly", priority: "0.3" },
- { path: "/sitemap", changefreq: "weekly", priority: "0.5" },
- { path: "/agb", changefreq: "monthly", priority: "0.3" },
- { path: "/404", changefreq: "monthly", priority: "0.1" },
- { path: "/Konfigurator", changefreq: "weekly", priority: "0.8" },
- ];
-
- staticPages.forEach((page) => {
- sitemap += `
- ${baseUrl}${page.path}
- ${currentDate}
- ${page.changefreq}
- ${page.priority}
-
-`;
- });
-
- // Category pages
- allCategories.forEach((category) => {
- if (category.seoName) {
- sitemap += `
- ${baseUrl}/Kategorie/${category.seoName}
- ${currentDate}
- weekly
- 0.8
-
-`;
- }
- });
-
- // Product pages
- allProducts.forEach((productSeoName) => {
- sitemap += `
- ${baseUrl}/Artikel/${productSeoName}
- ${currentDate}
- weekly
- 0.6
-
-`;
- });
-
- sitemap += ``;
-
- return sitemap;
-};
-
-const generateKonfiguratorMetaTags = (baseUrl, config) => {
- const description = "Unser interaktiver Growbox Konfigurator hilft dir dabei, das perfekte Indoor Growing Setup zusammenzustellen. Wähle aus verschiedenen Growbox-Größen, Beleuchtung, Belüftung und Extras. Bundle-Rabatte bis 36%!";
- const keywords = "Growbox Konfigurator, Indoor Growing, Growzelt, Beleuchtung, Belüftung, Growbox Setup, Indoor Garden";
-
- // Ensure URLs are properly formatted
- const canonicalUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
- const imageUrl = `${canonicalUrl}${config.images.placeholder}`; // Placeholder image
-
- return `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- `;
-};
-
-const generateRobotsTxt = (baseUrl) => {
- // Ensure URLs are properly formatted
- const canonicalUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
-
- const robotsTxt = `User-agent: *
-Allow: /
-Sitemap: ${canonicalUrl}/sitemap.xml
-Crawl-delay: 0
-`;
-
- return robotsTxt;
-};
-
-const generateProductsXml = (allProductsData = [], baseUrl, config) => {
- const currentDate = new Date().toISOString();
-
- // Validate input
- if (!Array.isArray(allProductsData) || allProductsData.length === 0) {
- throw new Error("No valid product data provided");
- }
-
- // Category mapping function
- const getGoogleProductCategory = (categoryId) => {
- const categoryMappings = {
- // Seeds & Plants
- 689: "Home & Garden > Plants > Seeds",
- 706: "Home & Garden > Plants", // Stecklinge (cuttings)
- 376: "Home & Garden > Plants > Plant & Herb Growing Kits", // Grow-Sets
-
- // Headshop & Accessories
- 709: "Arts & Entertainment > Hobbies & Creative Arts", // Headshop
- 711: "Arts & Entertainment > Hobbies & Creative Arts", // Bongs
- 714: "Arts & Entertainment > Hobbies & Creative Arts", // Zubehör
- 748: "Arts & Entertainment > Hobbies & Creative Arts", // Köpfe
- 749: "Arts & Entertainment > Hobbies & Creative Arts", // Chillums / Diffusoren / Kupplungen
- 896: "Electronics > Electronics Accessories", // Vaporizer
- 710: "Home & Garden > Kitchen & Dining > Kitchen Tools & Utensils", // Grinder
-
- // Measuring & Packaging
- 186: "Business & Industrial", // Wiegen & Verpacken
- 187: "Business & Industrial > Science & Laboratory > Lab Equipment", // Waagen
- 346: "Home & Garden > Kitchen & Dining > Food Storage", // Vakuumbeutel
- 355: "Home & Garden > Kitchen & Dining > Food Storage", // Boveda & Integra Boost
- 407: "Home & Garden > Kitchen & Dining > Food Storage", // Grove Bags
- 449: "Home & Garden > Kitchen & Dining > Food Storage", // Cliptütchen
- 539: "Home & Garden > Kitchen & Dining > Food Storage", // Gläser & Dosen
-
- // Lighting & Equipment
- 694: "Home & Garden > Lighting", // Lampen
- 261: "Home & Garden > Lighting", // Lampenzubehör
-
- // Plants & Growing
- 691: "Home & Garden > Lawn & Garden > Fertilizers", // Dünger
- 692: "Home & Garden > Lawn & Garden > Fertilizers", // Dünger - Zubehör
- 693: "Sporting Goods > Outdoor Recreation > Camping & Hiking > Tents", // Zelte
-
- // Pots & Containers
- 219: "Home & Garden > Decor > Planters & Pots", // Töpfe
- 220: "Home & Garden > Decor > Planters & Pots", // Untersetzer
- 301: "Home & Garden > Decor > Planters & Pots", // Stofftöpfe
- 317: "Home & Garden > Decor > Planters & Pots", // Air-Pot
- 364: "Home & Garden > Decor > Planters & Pots", // Kunststofftöpfe
- 292: "Home & Garden > Decor > Planters & Pots", // Trays & Fluttische
-
- // Ventilation & Climate
- 703: "Home & Garden > Outdoor Power Tools", // Abluft-Sets
- 247: "Home & Garden > Outdoor Power Tools", // Belüftung
- 214: "Home & Garden > Outdoor Power Tools", // Umluft-Ventilatoren
- 308: "Home & Garden > Outdoor Power Tools", // Ab- und Zuluft
- 609: "Home & Garden > Outdoor Power Tools", // Schalldämpfer
- 248: "Home & Garden > Pool & Spa > Pool & Spa Filters", // Aktivkohlefilter
- 392: "Home & Garden > Pool & Spa > Pool & Spa Filters", // Zuluftfilter
- 658: "Home & Garden > Climate Control > Dehumidifiers", // Luftbe- und entfeuchter
- 310: "Home & Garden > Climate Control > Heating", // Heizmatten
- 379: "Home & Garden > Household Supplies > Air Fresheners", // Geruchsneutralisation
-
- // Irrigation & Watering
- 221: "Home & Garden > Lawn & Garden > Watering Equipment", // Bewässerung
- 250: "Home & Garden > Lawn & Garden > Watering Equipment", // Schläuche
- 297: "Home & Garden > Lawn & Garden > Watering Equipment", // Pumpen
- 354: "Home & Garden > Lawn & Garden > Watering Equipment", // Sprüher
- 372: "Home & Garden > Lawn & Garden > Watering Equipment", // AutoPot
- 389: "Home & Garden > Lawn & Garden > Watering Equipment", // Blumat
- 405: "Home & Garden > Lawn & Garden > Watering Equipment", // Schläuche
- 425: "Home & Garden > Lawn & Garden > Watering Equipment", // Wassertanks
- 480: "Home & Garden > Lawn & Garden > Watering Equipment", // Tropfer
- 519: "Home & Garden > Lawn & Garden > Watering Equipment", // Pumpsprüher
-
- // Growing Media & Soils
- 242: "Home & Garden > Lawn & Garden > Fertilizers", // Böden
- 243: "Home & Garden > Lawn & Garden > Fertilizers", // Erde
- 269: "Home & Garden > Lawn & Garden > Fertilizers", // Kokos
- 580: "Home & Garden > Lawn & Garden > Fertilizers", // Perlite & Blähton
-
- // Propagation & Starting
- 286: "Home & Garden > Plants", // Anzucht
- 298: "Home & Garden > Plants", // Steinwolltrays
- 421: "Home & Garden > Plants", // Vermehrungszubehör
- 489: "Home & Garden > Plants", // EazyPlug & Jiffy
- 359: "Home & Garden > Outdoor Structures > Greenhouses", // Gewächshäuser
-
- // Tools & Equipment
- 373: "Home & Garden > Tools > Hand Tools", // GrowTool
- 403: "Home & Garden > Kitchen & Dining > Kitchen Tools & Utensils", // Messbecher & mehr
- 259: "Home & Garden > Tools > Hand Tools", // Pressen
- 280: "Home & Garden > Tools > Hand Tools", // Erntescheeren
- 258: "Home & Garden > Tools", // Ernte & Verarbeitung
- 278: "Home & Garden > Tools", // Extraktion
- 302: "Home & Garden > Tools", // Erntemaschinen
-
- // Hardware & Plumbing
- 222: "Hardware > Plumbing", // PE-Teile
- 374: "Hardware > Plumbing > Plumbing Fittings", // Verbindungsteile
-
- // Electronics & Control
- 314: "Electronics > Electronics Accessories", // Steuergeräte
- 408: "Electronics > Electronics Accessories", // GrowControl
- 344: "Business & Industrial > Science & Laboratory > Lab Equipment", // Messgeräte
- 555: "Business & Industrial > Science & Laboratory > Lab Equipment > Microscopes", // Mikroskope
-
- // Camping & Outdoor
- 226: "Sporting Goods > Outdoor Recreation > Camping & Hiking", // Zeltzubehör
-
- // Plant Care & Protection
- 239: "Home & Garden > Lawn & Garden > Pest Control", // Pflanzenschutz
- 240: "Home & Garden > Plants", // Anbauzubehör
-
- // Office & Media
- 424: "Office Supplies > Labels", // Etiketten & Schilder
- 387: "Media > Books", // Literatur
-
- // General categories
- 705: "Home & Garden", // Set-Konfigurator
- 686: "Home & Garden", // Zubehör
- 741: "Home & Garden", // Zubehör
- 294: "Home & Garden", // Zubehör
- 695: "Home & Garden", // Zubehör
- 293: "Home & Garden", // Trockennetze
- 4: "Home & Garden", // Sonstiges
- 450: "Home & Garden", // Restposten
- };
-
- return categoryMappings[categoryId] || "Home & Garden > Plants";
- };
-
- let productsXml = `
-
-
- ${config.descriptions.short}
- ${baseUrl}
- ${config.descriptions.short}
- ${currentDate}
- ${config.language}`;
-
- // Helper function to clean text content of problematic characters
- const cleanTextContent = (text) => {
- if (!text) return "";
-
- return text.toString()
- // Remove HTML tags
- .replace(/<[^>]*>/g, "")
- // Remove non-printable characters and control characters
- .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/g, '')
- // Remove BOM and other Unicode formatting characters
- .replace(/[\uFEFF\u200B-\u200D\u2060]/g, '')
- // Replace multiple whitespace with single space
- .replace(/\s+/g, ' ')
- // Remove leading/trailing whitespace
- .trim();
- };
-
- // Helper function to properly escape XML content and remove invalid characters
- const escapeXml = (unsafe) => {
- if (!unsafe) return "";
-
- // Convert to string and remove invalid XML characters
- const cleaned = unsafe.toString()
- // Remove control characters except tab (0x09), newline (0x0A), and carriage return (0x0D)
- .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '')
- // Remove invalid Unicode characters and surrogates
- .replace(/[\uD800-\uDFFF]/g, '')
- // Remove other problematic characters
- .replace(/[\uFFFE\uFFFF]/g, '')
- // Normalize whitespace
- .replace(/\s+/g, ' ')
- .trim();
-
- // Escape XML entities
- return cleaned
- .replace(/&/g, "&")
- .replace(//g, ">")
- .replace(/"/g, """)
- .replace(/'/g, "'");
- };
-
- let processedCount = 0;
- let skippedCount = 0;
-
- // Category IDs to skip (seeds, plants, headshop items)
- const skipCategoryIds = [689, 706, 709, 711, 714, 748, 749, 896, 710];
-
- // Add each product as an item
- allProductsData.forEach((product, index) => {
- try {
- // Skip products without essential data
- if (!product || !product.seoName) {
- skippedCount++;
- return;
- }
-
- // Skip products from excluded categories
- const productCategoryId = product.categoryId || product.category_id || product.category || null;
- if (productCategoryId && skipCategoryIds.includes(parseInt(productCategoryId))) {
- skippedCount++;
- return;
- }
-
- // Skip products without GTIN
- if (!product.gtin || !product.gtin.toString().trim()) {
- skippedCount++;
- return;
- }
-
- // Skip products without pictures
- if (!product.pictureList || !product.pictureList.trim()) {
- skippedCount++;
- return;
- }
-
- // Clean description for feed (remove HTML tags and limit length)
- const rawDescription = product.description
- ? cleanTextContent(product.description).substring(0, 500)
- : `${product.name || 'Product'} - Art.-Nr.: ${product.articleNumber || 'N/A'}`;
-
- const cleanDescription = escapeXml(rawDescription) || "Produktbeschreibung nicht verfügbar";
-
- // Clean product name
- const rawName = product.name || "Unnamed Product";
- const cleanName = escapeXml(cleanTextContent(rawName)) || "Unnamed Product";
-
- // Validate essential fields
- if (!cleanName || cleanName.length < 2) {
- skippedCount++;
- return;
- }
-
- // Generate product URL
- const productUrl = `${baseUrl}/Artikel/${encodeURIComponent(product.seoName)}`;
-
- // Generate image URL
- const imageUrl = product.pictureList && product.pictureList.trim()
- ? `${baseUrl}/assets/images/prod${product.pictureList.split(",")[0].trim()}.jpg`
- : `${baseUrl}/assets/images/nopicture.jpg`;
-
- // Generate brand (manufacturer)
- const rawBrand = product.manufacturer || config.brandName;
- const brand = escapeXml(cleanTextContent(rawBrand));
-
- // Generate condition (always new for this type of shop)
- const condition = "new";
-
- // Generate availability
- const availability = product.available ? "in stock" : "out of stock";
-
- // Generate price (ensure it's a valid number)
- const price = product.price && !isNaN(product.price)
- ? `${parseFloat(product.price).toFixed(2)} ${config.currency}`
- : `0.00 ${config.currency}`;
-
- // Skip products with price == 0
- if (!product.price || parseFloat(product.price) === 0) {
- skippedCount++;
- return;
- }
-
- // Generate GTIN/EAN if available
- const gtin = product.gtin ? escapeXml(product.gtin.toString().trim()) : null;
-
- // Generate product ID (using articleNumber or seoName)
- const rawProductId = product.articleNumber || product.seoName || `product_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`;
- const productId = escapeXml(rawProductId.toString().trim()) || `fallback_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`;
-
- // Get Google product category based on product's category ID
- const categoryId = product.categoryId || product.category_id || product.category || null;
- const googleCategory = getGoogleProductCategory(categoryId);
- const escapedGoogleCategory = escapeXml(googleCategory);
-
- // Build item XML with proper formatting
- productsXml += `
- -
- ${productId}
- ${cleanName}
- ${cleanDescription}
- ${productUrl}
- ${imageUrl}
- ${condition}
- ${availability}
- ${price}
-
- ${config.country}
- ${config.shipping.defaultService}
- ${config.shipping.defaultCost}
-
- ${brand}
- ${escapedGoogleCategory}
- Gartenbedarf`;
-
- // Add GTIN if available
- if (gtin && gtin.trim()) {
- productsXml += `
- ${gtin}`;
- }
-
- // Add weight if available
- if (product.weight && !isNaN(product.weight)) {
- productsXml += `
- ${parseFloat(product.weight).toFixed(2)} g`;
- }
-
- productsXml += `
-
`;
-
- processedCount++;
-
- } catch (itemError) {
- console.log(` ⚠️ Skipped product ${index + 1}: ${itemError.message}`);
- skippedCount++;
- }
- });
-
- productsXml += `
-
-`;
-
- console.log(` 📊 Processing summary: ${processedCount} products included, ${skippedCount} skipped`);
-
- return productsXml;
-};
-
-const generateLlmsTxt = (allCategories = [], allProductsData = [], baseUrl, config) => {
- const currentDate = new Date().toISOString().split("T")[0]; // YYYY-MM-DD format
-
- // Group products by category for statistics
- const productsByCategory = {};
- allProductsData.forEach((product) => {
- const categoryId = product.categoryId || 'uncategorized';
- if (!productsByCategory[categoryId]) {
- productsByCategory[categoryId] = [];
- }
- productsByCategory[categoryId].push(product);
- });
-
- // Find category names for organization
- const categoryMap = {};
- allCategories.forEach((cat) => {
- categoryMap[cat.id] = cat.name;
- });
-
- let llmsTxt = `# ${config.siteName} - Site Map for LLMs
-
-Generated: ${currentDate}
-Base URL: ${baseUrl}
-
-## About ${config.brandName}
-GrowHeads.de is a German online shop and local store in Dresden specializing in high-quality seeds, plants, and gardening supplies for cannabis cultivation.
-
-## Site Structure
-
-### Static Pages
-- **Home** - ${baseUrl}/
-- **Datenschutz (Privacy Policy)** - ${baseUrl}/datenschutz
-- **Impressum (Legal Notice)** - ${baseUrl}/impressum
-- **AGB (Terms & Conditions)** - ${baseUrl}/agb
-- **Widerrufsrecht (Right of Withdrawal)** - ${baseUrl}/widerrufsrecht
-- **Batteriegesetzhinweise (Battery Law Notice)** - ${baseUrl}/batteriegesetzhinweise
-- **Sitemap** - ${baseUrl}/sitemap
-- **Growbox Konfigurator** - ${baseUrl}/Konfigurator - Interactive tool to configure grow box setups with bundle discounts
-- **Profile** - ${baseUrl}/profile - User account and order management
-
-### Site Features
-- **Language**: German (${config.language})
-- **Currency**: ${config.currency} (Euro)
-- **Shipping**: ${config.country}
-- **Payment Methods**: Credit Cards, PayPal, Bank Transfer, Cash on Delivery, Cash on Pickup
-
-### Product Categories (${allCategories.length} categories)
-
-`;
-
- // Add categories with links to their detailed LLM files
- allCategories.forEach((category) => {
- if (category.seoName) {
- const productCount = productsByCategory[category.id]?.length || 0;
- const categorySlug = category.seoName.toLowerCase().replace(/[^a-z0-9]/g, '-');
- const productsPerPage = 50;
- const totalPages = Math.ceil(productCount / productsPerPage);
-
- llmsTxt += `#### ${category.name} (${productCount} products)`;
-
- if (totalPages > 1) {
- llmsTxt += `
-- **Product Catalog**: ${totalPages} pages available
-- **Page 1**: ${baseUrl}/llms-${categorySlug}-page-1.txt (Products 1-${Math.min(productsPerPage, productCount)})`;
-
- if (totalPages > 2) {
- llmsTxt += `
-- **Page 2**: ${baseUrl}/llms-${categorySlug}-page-2.txt (Products ${productsPerPage + 1}-${Math.min(productsPerPage * 2, productCount)})`;
- }
-
- if (totalPages > 3) {
- llmsTxt += `
-- **...**: Additional pages available`;
- }
-
- if (totalPages > 2) {
- llmsTxt += `
-- **Page ${totalPages}**: ${baseUrl}/llms-${categorySlug}-page-${totalPages}.txt (Products ${((totalPages - 1) * productsPerPage) + 1}-${productCount})`;
- }
-
- llmsTxt += `
-- **Access Pattern**: Replace "page-X" with desired page number (1-${totalPages})`;
- } else if (productCount > 0) {
- llmsTxt += `
-- **Product Catalog**: ${baseUrl}/llms-${categorySlug}-page-1.txt`;
- } else {
- llmsTxt += `
-- **Product Catalog**: No products available`;
- }
-
- llmsTxt += `
-
-`;
- }
- });
-
- llmsTxt += `
----
-
-*This sitemap is automatically generated during the site build process and includes all publicly accessible content. For technical inquiries, please refer to our contact information in the Impressum.*
-`;
-
- return llmsTxt;
-};
-
-const generateCategoryLlmsTxt = (category, categoryProducts = [], baseUrl, config, pageNumber = 1, productsPerPage = 50) => {
- const currentDate = new Date().toISOString().split("T")[0]; // YYYY-MM-DD format
- const categorySlug = category.seoName.toLowerCase().replace(/[^a-z0-9]/g, '-');
-
- // Calculate pagination
- const totalProducts = categoryProducts.length;
- const totalPages = Math.ceil(totalProducts / productsPerPage);
- const startIndex = (pageNumber - 1) * productsPerPage;
- const endIndex = Math.min(startIndex + productsPerPage, totalProducts);
- const pageProducts = categoryProducts.slice(startIndex, endIndex);
-
- let categoryLlmsTxt = `# ${category.name} - Product Catalog (Page ${pageNumber} of ${totalPages})
-
-Generated: ${currentDate}
-Base URL: ${baseUrl}
-Category: ${category.name} (ID: ${category.id})
-Category URL: ${baseUrl}/Kategorie/${category.seoName}
-
-## Category Overview
-This file contains products ${startIndex + 1}-${endIndex} of ${totalProducts} in the "${category.name}" category from ${config.siteName}.
-
-**Statistics:**
-- **Total Products in Category**: ${totalProducts}
-- **Products on This Page**: ${pageProducts.length}
-- **Current Page**: ${pageNumber} of ${totalPages}
-- **Category ID**: ${category.id}
-- **Category URL**: ${baseUrl}/Kategorie/${category.seoName}
-- **Back to Main Sitemap**: ${baseUrl}/llms.txt
-
-`;
-
- // Add navigation hints for LLMs
- if (totalPages > 1) {
- categoryLlmsTxt += `## Navigation for LLMs
-
-**How to access other pages in this category:**
-`;
-
- if (pageNumber > 1) {
- categoryLlmsTxt += `- **Previous Page**: ${baseUrl}/llms-${categorySlug}-page-${pageNumber - 1}.txt
-`;
- }
-
- if (pageNumber < totalPages) {
- categoryLlmsTxt += `- **Next Page**: ${baseUrl}/llms-${categorySlug}-page-${pageNumber + 1}.txt
-`;
- }
-
- categoryLlmsTxt += `- **First Page**: ${baseUrl}/llms-${categorySlug}-page-1.txt
-- **Last Page**: ${baseUrl}/llms-${categorySlug}-page-${totalPages}.txt
-
-**All pages in this category:**
-`;
-
- for (let i = 1; i <= totalPages; i++) {
- categoryLlmsTxt += `- **Page ${i}**: ${baseUrl}/llms-${categorySlug}-page-${i}.txt (Products ${((i-1) * productsPerPage) + 1}-${Math.min(i * productsPerPage, totalProducts)})
-`;
- }
-
- categoryLlmsTxt += `
-
-`;
- }
-
- if (pageProducts.length > 0) {
- pageProducts.forEach((product, index) => {
- if (product.seoName) {
- // Clean description for markdown (remove HTML tags and limit length)
- const cleanDescription = product.description
- ? product.description
- .replace(/<[^>]*>/g, "")
- .replace(/\n/g, " ")
- .trim()
- .substring(0, 300)
- : "";
-
- const globalIndex = startIndex + index + 1;
- categoryLlmsTxt += `## ${globalIndex}. ${product.name}
-
-- **Product URL**: ${baseUrl}/Artikel/${product.seoName}
-- **Article Number**: ${product.articleNumber || 'N/A'}
-- **Price**: €${product.price || '0.00'}
-- **Brand**: ${product.manufacturer || config.brandName}
-- **Availability**: ${product.available ? 'In Stock' : 'Out of Stock'}`;
-
- if (product.gtin) {
- categoryLlmsTxt += `
-- **GTIN**: ${product.gtin}`;
- }
-
- if (product.weight && !isNaN(product.weight)) {
- categoryLlmsTxt += `
-- **Weight**: ${product.weight}g`;
- }
-
- if (cleanDescription) {
- categoryLlmsTxt += `
-
-**Description:**
-${cleanDescription}${product.description && product.description.length > 300 ? '...' : ''}`;
- }
-
- categoryLlmsTxt += `
-
----
-
-`;
- }
- });
- } else {
- categoryLlmsTxt += `## No Products Available
-
-This category currently contains no products.
-
-`;
- }
-
- // Add footer navigation for convenience
- if (totalPages > 1) {
- categoryLlmsTxt += `## Page Navigation
-
-`;
- if (pageNumber > 1) {
- categoryLlmsTxt += `← [Previous Page](${baseUrl}/llms-${categorySlug}-page-${pageNumber - 1}.txt) | `;
- }
-
- categoryLlmsTxt += `[Category Overview](${baseUrl}/llms-${categorySlug}-page-1.txt)`;
-
- if (pageNumber < totalPages) {
- categoryLlmsTxt += ` | [Next Page](${baseUrl}/llms-${categorySlug}-page-${pageNumber + 1}.txt) →`;
- }
-
- categoryLlmsTxt += `
-
-`;
- }
-
- categoryLlmsTxt += `---
-
-*This category product list is automatically generated during the site build process. Product availability and pricing are updated in real-time on the main website.*
-`;
-
- return categoryLlmsTxt;
-};
-
-// 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,
- pageNumber,
- totalPages
- });
- }
-
- return pages;
-};
-
-module.exports = {
- generateProductMetaTags,
- generateProductJsonLd,
- generateCategoryJsonLd,
- generateHomepageMetaTags,
- generateHomepageJsonLd,
- generateSitemapJsonLd,
- generateKonfiguratorMetaTags,
- generateXmlSitemap,
- generateRobotsTxt,
- generateProductsXml,
- generateLlmsTxt,
- generateCategoryLlmsTxt,
- generateAllCategoryLlmsPages,
-};
+module.exports = require('./seo/index.cjs');
diff --git a/prerender/seo/category.cjs b/prerender/seo/category.cjs
new file mode 100644
index 0000000..77c9c78
--- /dev/null
+++ b/prerender/seo/category.cjs
@@ -0,0 +1,81 @@
+const generateCategoryJsonLd = (category, products = [], baseUrl, config) => {
+ const categoryUrl = `${baseUrl}/Kategorie/${category.seoName}`;
+
+ const jsonLd = {
+ "@context": "https://schema.org/",
+ "@type": "CollectionPage",
+ name: category.name,
+ url: categoryUrl,
+ description: `${category.name} - Entdecken Sie unsere Auswahl an hochwertigen Produkten`,
+ breadcrumb: {
+ "@type": "BreadcrumbList",
+ itemListElement: [
+ {
+ "@type": "ListItem",
+ position: 1,
+ name: "Home",
+ item: baseUrl,
+ },
+ {
+ "@type": "ListItem",
+ position: 2,
+ name: category.name,
+ item: categoryUrl,
+ },
+ ],
+ },
+ };
+
+ // Add product list if products are available
+ if (products && products.length > 0) {
+ jsonLd.mainEntity = {
+ "@type": "ItemList",
+ numberOfItems: products.length,
+ itemListElement: products.slice(0, 20).map((product, index) => ({
+ "@type": "ListItem",
+ position: index + 1,
+ item: {
+ "@type": "Product",
+ name: product.name,
+ url: `${baseUrl}/Artikel/${product.seoName}`,
+ image:
+ product.pictureList && product.pictureList.trim()
+ ? `${baseUrl}/assets/images/prod${product.pictureList
+ .split(",")[0]
+ .trim()}.jpg`
+ : `${baseUrl}/assets/images/nopicture.jpg`,
+ description: product.description
+ ? product.description.replace(/<[^>]*>/g, "").substring(0, 200)
+ : `${product.name} - Hochwertiges Growshop Produkt`,
+ sku: product.articleNumber || product.seoName,
+ brand: {
+ "@type": "Brand",
+ name: product.manufacturer || config.brandName,
+ },
+ offers: {
+ "@type": "Offer",
+ url: `${baseUrl}/Artikel/${product.seoName}`,
+ price: product.price && !isNaN(product.price) ? product.price.toString() : "0.00",
+ priceCurrency: config.currency,
+ availability: product.available
+ ? "https://schema.org/InStock"
+ : "https://schema.org/OutOfStock",
+ seller: {
+ "@type": "Organization",
+ name: config.brandName,
+ },
+ itemCondition: "https://schema.org/NewCondition",
+ },
+ },
+ })),
+ };
+ }
+
+ return ``;
+};
+
+module.exports = {
+ generateCategoryJsonLd,
+};
\ No newline at end of file
diff --git a/prerender/seo/feeds.cjs b/prerender/seo/feeds.cjs
new file mode 100644
index 0000000..fc43c9b
--- /dev/null
+++ b/prerender/seo/feeds.cjs
@@ -0,0 +1,344 @@
+const generateRobotsTxt = (baseUrl) => {
+ // Ensure URLs are properly formatted
+ const canonicalUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
+
+ const robotsTxt = `User-agent: *
+Allow: /
+Sitemap: ${canonicalUrl}/sitemap.xml
+Crawl-delay: 0
+`;
+
+ return robotsTxt;
+};
+
+const generateProductsXml = (allProductsData = [], baseUrl, config) => {
+ const currentDate = new Date().toISOString();
+
+ // Validate input
+ if (!Array.isArray(allProductsData) || allProductsData.length === 0) {
+ throw new Error("No valid product data provided");
+ }
+
+ // Category mapping function
+ const getGoogleProductCategory = (categoryId) => {
+ const categoryMappings = {
+ // Seeds & Plants
+ 689: "Home & Garden > Plants > Seeds",
+ 706: "Home & Garden > Plants", // Stecklinge (cuttings)
+ 376: "Home & Garden > Plants > Plant & Herb Growing Kits", // Grow-Sets
+
+ // Headshop & Accessories
+ 709: "Arts & Entertainment > Hobbies & Creative Arts", // Headshop
+ 711: "Arts & Entertainment > Hobbies & Creative Arts", // Bongs
+ 714: "Arts & Entertainment > Hobbies & Creative Arts", // Zubehör
+ 748: "Arts & Entertainment > Hobbies & Creative Arts", // Köpfe
+ 749: "Arts & Entertainment > Hobbies & Creative Arts", // Chillums / Diffusoren / Kupplungen
+ 896: "Electronics > Electronics Accessories", // Vaporizer
+ 710: "Home & Garden > Kitchen & Dining > Kitchen Tools & Utensils", // Grinder
+
+ // Measuring & Packaging
+ 186: "Business & Industrial", // Wiegen & Verpacken
+ 187: "Business & Industrial > Science & Laboratory > Lab Equipment", // Waagen
+ 346: "Home & Garden > Kitchen & Dining > Food Storage", // Vakuumbeutel
+ 355: "Home & Garden > Kitchen & Dining > Food Storage", // Boveda & Integra Boost
+ 407: "Home & Garden > Kitchen & Dining > Food Storage", // Grove Bags
+ 449: "Home & Garden > Kitchen & Dining > Food Storage", // Cliptütchen
+ 539: "Home & Garden > Kitchen & Dining > Food Storage", // Gläser & Dosen
+
+ // Lighting & Equipment
+ 694: "Home & Garden > Lighting", // Lampen
+ 261: "Home & Garden > Lighting", // Lampenzubehör
+
+ // Plants & Growing
+ 691: "Home & Garden > Lawn & Garden > Fertilizers", // Dünger
+ 692: "Home & Garden > Lawn & Garden > Fertilizers", // Dünger - Zubehör
+ 693: "Sporting Goods > Outdoor Recreation > Camping & Hiking > Tents", // Zelte
+
+ // Pots & Containers
+ 219: "Home & Garden > Decor > Planters & Pots", // Töpfe
+ 220: "Home & Garden > Decor > Planters & Pots", // Untersetzer
+ 301: "Home & Garden > Decor > Planters & Pots", // Stofftöpfe
+ 317: "Home & Garden > Decor > Planters & Pots", // Air-Pot
+ 364: "Home & Garden > Decor > Planters & Pots", // Kunststofftöpfe
+ 292: "Home & Garden > Decor > Planters & Pots", // Trays & Fluttische
+
+ // Ventilation & Climate
+ 703: "Home & Garden > Outdoor Power Tools", // Abluft-Sets
+ 247: "Home & Garden > Outdoor Power Tools", // Belüftung
+ 214: "Home & Garden > Outdoor Power Tools", // Umluft-Ventilatoren
+ 308: "Home & Garden > Outdoor Power Tools", // Ab- und Zuluft
+ 609: "Home & Garden > Outdoor Power Tools", // Schalldämpfer
+ 248: "Home & Garden > Pool & Spa > Pool & Spa Filters", // Aktivkohlefilter
+ 392: "Home & Garden > Pool & Spa > Pool & Spa Filters", // Zuluftfilter
+ 658: "Home & Garden > Climate Control > Dehumidifiers", // Luftbe- und entfeuchter
+ 310: "Home & Garden > Climate Control > Heating", // Heizmatten
+ 379: "Home & Garden > Household Supplies > Air Fresheners", // Geruchsneutralisation
+
+ // Irrigation & Watering
+ 221: "Home & Garden > Lawn & Garden > Watering Equipment", // Bewässerung
+ 250: "Home & Garden > Lawn & Garden > Watering Equipment", // Schläuche
+ 297: "Home & Garden > Lawn & Garden > Watering Equipment", // Pumpen
+ 354: "Home & Garden > Lawn & Garden > Watering Equipment", // Sprüher
+ 372: "Home & Garden > Lawn & Garden > Watering Equipment", // AutoPot
+ 389: "Home & Garden > Lawn & Garden > Watering Equipment", // Blumat
+ 405: "Home & Garden > Lawn & Garden > Watering Equipment", // Schläuche
+ 425: "Home & Garden > Lawn & Garden > Watering Equipment", // Wassertanks
+ 480: "Home & Garden > Lawn & Garden > Watering Equipment", // Tropfer
+ 519: "Home & Garden > Lawn & Garden > Watering Equipment", // Pumpsprüher
+
+ // Growing Media & Soils
+ 242: "Home & Garden > Lawn & Garden > Fertilizers", // Böden
+ 243: "Home & Garden > Lawn & Garden > Fertilizers", // Erde
+ 269: "Home & Garden > Lawn & Garden > Fertilizers", // Kokos
+ 580: "Home & Garden > Lawn & Garden > Fertilizers", // Perlite & Blähton
+
+ // Propagation & Starting
+ 286: "Home & Garden > Plants", // Anzucht
+ 298: "Home & Garden > Plants", // Steinwolltrays
+ 421: "Home & Garden > Plants", // Vermehrungszubehör
+ 489: "Home & Garden > Plants", // EazyPlug & Jiffy
+ 359: "Home & Garden > Outdoor Structures > Greenhouses", // Gewächshäuser
+
+ // Tools & Equipment
+ 373: "Home & Garden > Tools > Hand Tools", // GrowTool
+ 403: "Home & Garden > Kitchen & Dining > Kitchen Tools & Utensils", // Messbecher & mehr
+ 259: "Home & Garden > Tools > Hand Tools", // Pressen
+ 280: "Home & Garden > Tools > Hand Tools", // Erntescheeren
+ 258: "Home & Garden > Tools", // Ernte & Verarbeitung
+ 278: "Home & Garden > Tools", // Extraktion
+ 302: "Home & Garden > Tools", // Erntemaschinen
+
+ // Hardware & Plumbing
+ 222: "Hardware > Plumbing", // PE-Teile
+ 374: "Hardware > Plumbing > Plumbing Fittings", // Verbindungsteile
+
+ // Electronics & Control
+ 314: "Electronics > Electronics Accessories", // Steuergeräte
+ 408: "Electronics > Electronics Accessories", // GrowControl
+ 344: "Business & Industrial > Science & Laboratory > Lab Equipment", // Messgeräte
+ 555: "Business & Industrial > Science & Laboratory > Lab Equipment > Microscopes", // Mikroskope
+
+ // Camping & Outdoor
+ 226: "Sporting Goods > Outdoor Recreation > Camping & Hiking", // Zeltzubehör
+
+ // Plant Care & Protection
+ 239: "Home & Garden > Lawn & Garden > Pest Control", // Pflanzenschutz
+ 240: "Home & Garden > Plants", // Anbauzubehör
+
+ // Office & Media
+ 424: "Office Supplies > Labels", // Etiketten & Schilder
+ 387: "Media > Books", // Literatur
+
+ // General categories
+ 705: "Home & Garden", // Set-Konfigurator
+ 686: "Home & Garden", // Zubehör
+ 741: "Home & Garden", // Zubehör
+ 294: "Home & Garden", // Zubehör
+ 695: "Home & Garden", // Zubehör
+ 293: "Home & Garden", // Trockennetze
+ 4: "Home & Garden", // Sonstiges
+ 450: "Home & Garden", // Restposten
+ };
+
+ return categoryMappings[categoryId] || "Home & Garden > Plants";
+ };
+
+ let productsXml = `
+
+
+ ${config.descriptions.short}
+ ${baseUrl}
+ ${config.descriptions.short}
+ ${currentDate}
+ ${config.language}`;
+
+ // Helper function to clean text content of problematic characters
+ const cleanTextContent = (text) => {
+ if (!text) return "";
+
+ return text.toString()
+ // Remove HTML tags
+ .replace(/<[^>]*>/g, "")
+ // Remove non-printable characters and control characters
+ .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/g, '')
+ // Remove BOM and other Unicode formatting characters
+ .replace(/[\uFEFF\u200B-\u200D\u2060]/g, '')
+ // Replace multiple whitespace with single space
+ .replace(/\s+/g, ' ')
+ // Remove leading/trailing whitespace
+ .trim();
+ };
+
+ // Helper function to properly escape XML content and remove invalid characters
+ const escapeXml = (unsafe) => {
+ if (!unsafe) return "";
+
+ // Convert to string and remove invalid XML characters
+ const cleaned = unsafe.toString()
+ // Remove control characters except tab (0x09), newline (0x0A), and carriage return (0x0D)
+ .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '')
+ // Remove invalid Unicode characters and surrogates
+ .replace(/[\uD800-\uDFFF]/g, '')
+ // Remove other problematic characters
+ .replace(/[\uFFFE\uFFFF]/g, '')
+ // Normalize whitespace
+ .replace(/\s+/g, ' ')
+ .trim();
+
+ // Escape XML entities
+ return cleaned
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+ };
+
+ let processedCount = 0;
+ let skippedCount = 0;
+
+ // Category IDs to skip (seeds, plants, headshop items)
+ const skipCategoryIds = [689, 706, 709, 711, 714, 748, 749, 896, 710];
+
+ // Add each product as an item
+ allProductsData.forEach((product, index) => {
+ try {
+ // Skip products without essential data
+ if (!product || !product.seoName) {
+ skippedCount++;
+ return;
+ }
+
+ // Skip products from excluded categories
+ const productCategoryId = product.categoryId || product.category_id || product.category || null;
+ if (productCategoryId && skipCategoryIds.includes(parseInt(productCategoryId))) {
+ skippedCount++;
+ return;
+ }
+
+ // Skip products without GTIN
+ if (!product.gtin || !product.gtin.toString().trim()) {
+ skippedCount++;
+ return;
+ }
+
+ // Skip products without pictures
+ if (!product.pictureList || !product.pictureList.trim()) {
+ skippedCount++;
+ return;
+ }
+
+ // Clean description for feed (remove HTML tags and limit length)
+ const rawDescription = product.description
+ ? cleanTextContent(product.description).substring(0, 500)
+ : `${product.name || 'Product'} - Art.-Nr.: ${product.articleNumber || 'N/A'}`;
+
+ const cleanDescription = escapeXml(rawDescription) || "Produktbeschreibung nicht verfügbar";
+
+ // Clean product name
+ const rawName = product.name || "Unnamed Product";
+ const cleanName = escapeXml(cleanTextContent(rawName)) || "Unnamed Product";
+
+ // Validate essential fields
+ if (!cleanName || cleanName.length < 2) {
+ skippedCount++;
+ return;
+ }
+
+ // Generate product URL
+ const productUrl = `${baseUrl}/Artikel/${encodeURIComponent(product.seoName)}`;
+
+ // Generate image URL
+ const imageUrl = product.pictureList && product.pictureList.trim()
+ ? `${baseUrl}/assets/images/prod${product.pictureList.split(",")[0].trim()}.jpg`
+ : `${baseUrl}/assets/images/nopicture.jpg`;
+
+ // Generate brand (manufacturer)
+ const rawBrand = product.manufacturer || config.brandName;
+ const brand = escapeXml(cleanTextContent(rawBrand));
+
+ // Generate condition (always new for this type of shop)
+ const condition = "new";
+
+ // Generate availability
+ const availability = product.available ? "in stock" : "out of stock";
+
+ // Generate price (ensure it's a valid number)
+ const price = product.price && !isNaN(product.price)
+ ? `${parseFloat(product.price).toFixed(2)} ${config.currency}`
+ : `0.00 ${config.currency}`;
+
+ // Skip products with price == 0
+ if (!product.price || parseFloat(product.price) === 0) {
+ skippedCount++;
+ return;
+ }
+
+ // Generate GTIN/EAN if available
+ const gtin = product.gtin ? escapeXml(product.gtin.toString().trim()) : null;
+
+ // Generate product ID (using articleNumber or seoName)
+ const rawProductId = product.articleNumber || product.seoName || `product_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`;
+ const productId = escapeXml(rawProductId.toString().trim()) || `fallback_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`;
+
+ // Get Google product category based on product's category ID
+ const categoryId = product.categoryId || product.category_id || product.category || null;
+ const googleCategory = getGoogleProductCategory(categoryId);
+ const escapedGoogleCategory = escapeXml(googleCategory);
+
+ // Build item XML with proper formatting
+ productsXml += `
+ -
+ ${productId}
+ ${cleanName}
+ ${cleanDescription}
+ ${productUrl}
+ ${imageUrl}
+ ${condition}
+ ${availability}
+ ${price}
+
+ ${config.country}
+ ${config.shipping.defaultService}
+ ${config.shipping.defaultCost}
+
+ ${brand}
+ ${escapedGoogleCategory}
+ Gartenbedarf`;
+
+ // Add GTIN if available
+ if (gtin && gtin.trim()) {
+ productsXml += `
+ ${gtin}`;
+ }
+
+ // Add weight if available
+ if (product.weight && !isNaN(product.weight)) {
+ productsXml += `
+ ${parseFloat(product.weight).toFixed(2)} g`;
+ }
+
+ productsXml += `
+
`;
+
+ processedCount++;
+
+ } catch (itemError) {
+ console.log(` ⚠️ Skipped product ${index + 1}: ${itemError.message}`);
+ skippedCount++;
+ }
+ });
+
+ productsXml += `
+
+`;
+
+ console.log(` 📊 Processing summary: ${processedCount} products included, ${skippedCount} skipped`);
+
+ return productsXml;
+};
+
+module.exports = {
+ generateRobotsTxt,
+ generateProductsXml,
+};
\ No newline at end of file
diff --git a/prerender/seo/homepage.cjs b/prerender/seo/homepage.cjs
new file mode 100644
index 0000000..a560c68
--- /dev/null
+++ b/prerender/seo/homepage.cjs
@@ -0,0 +1,215 @@
+const generateHomepageMetaTags = (baseUrl, config) => {
+ const description = config.descriptions.long;
+ const keywords = config.keywords;
+ const imageUrl = `${baseUrl}${config.images.logo}`;
+
+ // Ensure URLs are properly formatted
+ const canonicalUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
+
+ return `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+};
+
+const generateHomepageJsonLd = (baseUrl, config, categories = []) => {
+ // Ensure URLs are properly formatted
+ const canonicalUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
+ const logoUrl = `${canonicalUrl}${config.images.logo}`;
+
+ const websiteJsonLd = {
+ "@context": "https://schema.org/",
+ "@type": "WebSite",
+ name: config.brandName,
+ url: canonicalUrl,
+ description: config.descriptions.long,
+ publisher: {
+ "@type": "Organization",
+ name: config.brandName,
+ url: canonicalUrl,
+ logo: {
+ "@type": "ImageObject",
+ url: logoUrl,
+ },
+ },
+ potentialAction: {
+ "@type": "SearchAction",
+ target: `${canonicalUrl}/search?q={search_term_string}`,
+ query: "required name=search_term_string"
+ },
+ mainEntity: {
+ "@type": "WebPage",
+ name: "Sitemap",
+ url: `${canonicalUrl}/sitemap`,
+ description: "Vollständige Sitemap mit allen Kategorien und Seiten",
+ },
+ sameAs: [
+ // Add your social media URLs here if available
+ ],
+ };
+
+ // Organization/LocalBusiness Schema for rich results
+ const organizationJsonLd = {
+ "@context": "https://schema.org",
+ "@type": "LocalBusiness",
+ "name": config.brandName,
+ "alternateName": config.siteName,
+ "description": config.descriptions.long,
+ "url": canonicalUrl,
+ "logo": logoUrl,
+ "image": logoUrl,
+ "telephone": "015208491860",
+ "email": "service@growheads.de",
+ "address": {
+ "@type": "PostalAddress",
+ "streetAddress": "Trachenberger Strasse 14",
+ "addressLocality": "Dresden",
+ "postalCode": "01129",
+ "addressCountry": "DE",
+ "addressRegion": "Sachsen"
+ },
+ "geo": {
+ "@type": "GeoCoordinates",
+ "latitude": "51.083675",
+ "longitude": "13.727215"
+ },
+ "openingHours": [
+ "Mo-Fr 10:00:00-20:00:00",
+ "Sa 11:00:00-19:00:00"
+ ],
+ "paymentAccepted": "Cash, Credit Card, PayPal, Bank Transfer",
+ "currenciesAccepted": "EUR",
+ "priceRange": "€€",
+ "areaServed": {
+ "@type": "Country",
+ "name": "Germany"
+ },
+ "contactPoint": [
+ {
+ "@type": "ContactPoint",
+ "telephone": "015208491860",
+ "contactType": "customer service",
+ "availableLanguage": "German",
+ "hoursAvailable": {
+ "@type": "OpeningHoursSpecification",
+ "dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
+ "opens": "10:00:00",
+ "closes": "20:00:00"
+ }
+ },
+ {
+ "@type": "ContactPoint",
+ "email": "service@growheads.de",
+ "contactType": "customer service",
+ "availableLanguage": "German"
+ }
+ ],
+ "sameAs": [
+ // Add social media URLs when available
+ // "https://www.facebook.com/growheads",
+ // "https://www.instagram.com/growheads"
+ ]
+ };
+
+ // FAQPage Schema for common questions
+ const faqJsonLd = {
+ "@context": "https://schema.org",
+ "@type": "FAQPage",
+ "mainEntity": [
+ {
+ "@type": "Question",
+ "name": "Welche Zahlungsmethoden akzeptiert GrowHeads?",
+ "acceptedAnswer": {
+ "@type": "Answer",
+ "text": "Wir akzeptieren Kreditkarten, PayPal, Banküberweisung, Nachnahme und Barzahlung bei Abholung in unserem Laden in Dresden."
+ }
+ },
+ {
+ "@type": "Question",
+ "name": "Liefert GrowHeads deutschlandweit?",
+ "acceptedAnswer": {
+ "@type": "Answer",
+ "text": "Ja, wir liefern deutschlandweit. Zusätzlich haben wir einen Laden in Dresden (Trachenberger Strasse 14) für lokale Kunden."
+ }
+ },
+ {
+ "@type": "Question",
+ "name": "Welche Produkte bietet GrowHeads?",
+ "acceptedAnswer": {
+ "@type": "Answer",
+ "text": "Wir bieten ein komplettes Sortiment für den Indoor-Anbau: Beleuchtung, Belüftung, Dünger, Töpfe, Zelte, Messgeräte und vieles mehr für professionelle Zuchtanlagen."
+ }
+ },
+ {
+ "@type": "Question",
+ "name": "Hat GrowHeads einen physischen Laden?",
+ "acceptedAnswer": {
+ "@type": "Answer",
+ "text": "Ja, unser Laden befindet sich in Dresden, Trachenberger Strasse 14. Öffnungszeiten: Mo-Fr 10-20 Uhr, Sa 11-19 Uhr. Sie können auch online bestellen."
+ }
+ },
+ {
+ "@type": "Question",
+ "name": "Bietet GrowHeads Beratung zum Indoor-Anbau?",
+ "acceptedAnswer": {
+ "@type": "Answer",
+ "text": "Ja, unser erfahrenes Team berät Sie gerne zu allen Aspekten des Indoor-Anbaus. Kontaktieren Sie uns telefonisch unter 015208491860 oder besuchen Sie unseren Laden."
+ }
+ }
+ ]
+ };
+
+ // Generate ItemList for all categories (more appropriate for homepage)
+ const categoriesListJsonLd = {
+ "@context": "https://schema.org",
+ "@type": "ItemList",
+ "name": "Produktkategorien",
+ "description": "Alle verfügbaren Produktkategorien in unserem Online-Shop",
+ "numberOfItems": categories.filter(category => category.seoName).length,
+ "itemListElement": categories
+ .filter(category => category.seoName) // Only include categories with seoName
+ .map((category, index) => ({
+ "@type": "ListItem",
+ "position": index + 1,
+ "item": {
+ "@type": "Thing",
+ "name": category.name,
+ "url": `${canonicalUrl}/Kategorie/${category.seoName}`
+ }
+ }))
+ };
+
+ // Return all JSON-LD scripts
+ const websiteScript = ``;
+ const organizationScript = ``;
+ const faqScript = ``;
+ const categoriesScript = categories.length > 0
+ ? ``
+ : '';
+
+ return websiteScript + '\n' + organizationScript + '\n' + faqScript + (categoriesScript ? '\n' + categoriesScript : '');
+};
+
+module.exports = {
+ generateHomepageMetaTags,
+ generateHomepageJsonLd,
+};
\ No newline at end of file
diff --git a/prerender/seo/index.cjs b/prerender/seo/index.cjs
new file mode 100644
index 0000000..ee08850
--- /dev/null
+++ b/prerender/seo/index.cjs
@@ -0,0 +1,64 @@
+// Import all SEO functions from their respective modules
+const {
+ generateProductMetaTags,
+ generateProductJsonLd,
+} = require('./product.cjs');
+
+const {
+ generateCategoryJsonLd,
+} = require('./category.cjs');
+
+const {
+ generateHomepageMetaTags,
+ generateHomepageJsonLd,
+} = require('./homepage.cjs');
+
+const {
+ generateSitemapJsonLd,
+ generateXmlSitemap,
+} = require('./sitemap.cjs');
+
+const {
+ generateKonfiguratorMetaTags,
+} = require('./konfigurator.cjs');
+
+const {
+ generateRobotsTxt,
+ generateProductsXml,
+} = require('./feeds.cjs');
+
+const {
+ generateLlmsTxt,
+ generateCategoryLlmsTxt,
+ generateAllCategoryLlmsPages,
+} = require('./llms.cjs');
+
+// Export all functions for use in the main application
+module.exports = {
+ // Product functions
+ generateProductMetaTags,
+ generateProductJsonLd,
+
+ // Category functions
+ generateCategoryJsonLd,
+
+ // Homepage functions
+ generateHomepageMetaTags,
+ generateHomepageJsonLd,
+
+ // Sitemap functions
+ generateSitemapJsonLd,
+ generateXmlSitemap,
+
+ // Konfigurator functions
+ generateKonfiguratorMetaTags,
+
+ // Feed/Export functions
+ generateRobotsTxt,
+ generateProductsXml,
+
+ // LLMs/AI functions
+ generateLlmsTxt,
+ generateCategoryLlmsTxt,
+ generateAllCategoryLlmsPages,
+};
\ No newline at end of file
diff --git a/prerender/seo/konfigurator.cjs b/prerender/seo/konfigurator.cjs
new file mode 100644
index 0000000..1478388
--- /dev/null
+++ b/prerender/seo/konfigurator.cjs
@@ -0,0 +1,36 @@
+const generateKonfiguratorMetaTags = (baseUrl, config) => {
+ const description = "Unser interaktiver Growbox Konfigurator hilft dir dabei, das perfekte Indoor Growing Setup zusammenzustellen. Wähle aus verschiedenen Growbox-Größen, Beleuchtung, Belüftung und Extras. Bundle-Rabatte bis 36%!";
+ const keywords = "Growbox Konfigurator, Indoor Growing, Growzelt, Beleuchtung, Belüftung, Growbox Setup, Indoor Garden";
+
+ // Ensure URLs are properly formatted
+ const canonicalUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
+ const imageUrl = `${canonicalUrl}${config.images.placeholder}`; // Placeholder image
+
+ return `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+};
+
+module.exports = {
+ generateKonfiguratorMetaTags,
+};
\ No newline at end of file
diff --git a/prerender/seo/llms.cjs b/prerender/seo/llms.cjs
new file mode 100644
index 0000000..dffdc28
--- /dev/null
+++ b/prerender/seo/llms.cjs
@@ -0,0 +1,277 @@
+const generateLlmsTxt = (allCategories = [], allProductsData = [], baseUrl, config) => {
+ const currentDate = new Date().toISOString().split("T")[0]; // YYYY-MM-DD format
+
+ // Group products by category for statistics
+ const productsByCategory = {};
+ allProductsData.forEach((product) => {
+ const categoryId = product.categoryId || 'uncategorized';
+ if (!productsByCategory[categoryId]) {
+ productsByCategory[categoryId] = [];
+ }
+ productsByCategory[categoryId].push(product);
+ });
+
+ // Find category names for organization
+ const categoryMap = {};
+ allCategories.forEach((cat) => {
+ categoryMap[cat.id] = cat.name;
+ });
+
+ let llmsTxt = `# ${config.siteName} - Site Map for LLMs
+
+Generated: ${currentDate}
+Base URL: ${baseUrl}
+
+## About ${config.brandName}
+GrowHeads.de is a German online shop and local store in Dresden specializing in high-quality seeds, plants, and gardening supplies for cannabis cultivation.
+
+## Site Structure
+
+### Static Pages
+- **Home** - ${baseUrl}/
+- **Datenschutz (Privacy Policy)** - ${baseUrl}/datenschutz
+- **Impressum (Legal Notice)** - ${baseUrl}/impressum
+- **AGB (Terms & Conditions)** - ${baseUrl}/agb
+- **Widerrufsrecht (Right of Withdrawal)** - ${baseUrl}/widerrufsrecht
+- **Batteriegesetzhinweise (Battery Law Notice)** - ${baseUrl}/batteriegesetzhinweise
+- **Sitemap** - ${baseUrl}/sitemap
+- **Growbox Konfigurator** - ${baseUrl}/Konfigurator - Interactive tool to configure grow box setups with bundle discounts
+- **Profile** - ${baseUrl}/profile - User account and order management
+
+### Site Features
+- **Language**: German (${config.language})
+- **Currency**: ${config.currency} (Euro)
+- **Shipping**: ${config.country}
+- **Payment Methods**: Credit Cards, PayPal, Bank Transfer, Cash on Delivery, Cash on Pickup
+
+### Product Categories (${allCategories.length} categories)
+
+`;
+
+ // Add categories with links to their detailed LLM files
+ allCategories.forEach((category) => {
+ if (category.seoName) {
+ const productCount = productsByCategory[category.id]?.length || 0;
+ const categorySlug = category.seoName.toLowerCase().replace(/[^a-z0-9]/g, '-');
+ const productsPerPage = 50;
+ const totalPages = Math.ceil(productCount / productsPerPage);
+
+ llmsTxt += `#### ${category.name} (${productCount} products)`;
+
+ if (totalPages > 1) {
+ llmsTxt += `
+- **Product Catalog**: ${totalPages} pages available
+- **Page 1**: ${baseUrl}/llms-${categorySlug}-page-1.txt (Products 1-${Math.min(productsPerPage, productCount)})`;
+
+ if (totalPages > 2) {
+ llmsTxt += `
+- **Page 2**: ${baseUrl}/llms-${categorySlug}-page-2.txt (Products ${productsPerPage + 1}-${Math.min(productsPerPage * 2, productCount)})`;
+ }
+
+ if (totalPages > 3) {
+ llmsTxt += `
+- **...**: Additional pages available`;
+ }
+
+ if (totalPages > 2) {
+ llmsTxt += `
+- **Page ${totalPages}**: ${baseUrl}/llms-${categorySlug}-page-${totalPages}.txt (Products ${((totalPages - 1) * productsPerPage) + 1}-${productCount})`;
+ }
+
+ llmsTxt += `
+- **Access Pattern**: Replace "page-X" with desired page number (1-${totalPages})`;
+ } else if (productCount > 0) {
+ llmsTxt += `
+- **Product Catalog**: ${baseUrl}/llms-${categorySlug}-page-1.txt`;
+ } else {
+ llmsTxt += `
+- **Product Catalog**: No products available`;
+ }
+
+ llmsTxt += `
+
+`;
+ }
+ });
+
+ llmsTxt += `
+---
+
+*This sitemap is automatically generated during the site build process and includes all publicly accessible content. For technical inquiries, please refer to our contact information in the Impressum.*
+`;
+
+ return llmsTxt;
+};
+
+const generateCategoryLlmsTxt = (category, categoryProducts = [], baseUrl, config, pageNumber = 1, productsPerPage = 50) => {
+ const currentDate = new Date().toISOString().split("T")[0]; // YYYY-MM-DD format
+ const categorySlug = category.seoName.toLowerCase().replace(/[^a-z0-9]/g, '-');
+
+ // Calculate pagination
+ const totalProducts = categoryProducts.length;
+ const totalPages = Math.ceil(totalProducts / productsPerPage);
+ const startIndex = (pageNumber - 1) * productsPerPage;
+ const endIndex = Math.min(startIndex + productsPerPage, totalProducts);
+ const pageProducts = categoryProducts.slice(startIndex, endIndex);
+
+ let categoryLlmsTxt = `# ${category.name} - Product Catalog (Page ${pageNumber} of ${totalPages})
+
+Generated: ${currentDate}
+Base URL: ${baseUrl}
+Category: ${category.name} (ID: ${category.id})
+Category URL: ${baseUrl}/Kategorie/${category.seoName}
+
+## Category Overview
+This file contains products ${startIndex + 1}-${endIndex} of ${totalProducts} in the "${category.name}" category from ${config.siteName}.
+
+**Statistics:**
+- **Total Products in Category**: ${totalProducts}
+- **Products on This Page**: ${pageProducts.length}
+- **Current Page**: ${pageNumber} of ${totalPages}
+- **Category ID**: ${category.id}
+- **Category URL**: ${baseUrl}/Kategorie/${category.seoName}
+- **Back to Main Sitemap**: ${baseUrl}/llms.txt
+
+`;
+
+ // Add navigation hints for LLMs
+ if (totalPages > 1) {
+ categoryLlmsTxt += `## Navigation for LLMs
+
+**How to access other pages in this category:**
+`;
+
+ if (pageNumber > 1) {
+ categoryLlmsTxt += `- **Previous Page**: ${baseUrl}/llms-${categorySlug}-page-${pageNumber - 1}.txt
+`;
+ }
+
+ if (pageNumber < totalPages) {
+ categoryLlmsTxt += `- **Next Page**: ${baseUrl}/llms-${categorySlug}-page-${pageNumber + 1}.txt
+`;
+ }
+
+ categoryLlmsTxt += `- **First Page**: ${baseUrl}/llms-${categorySlug}-page-1.txt
+- **Last Page**: ${baseUrl}/llms-${categorySlug}-page-${totalPages}.txt
+
+**All pages in this category:**
+`;
+
+ for (let i = 1; i <= totalPages; i++) {
+ categoryLlmsTxt += `- **Page ${i}**: ${baseUrl}/llms-${categorySlug}-page-${i}.txt (Products ${((i-1) * productsPerPage) + 1}-${Math.min(i * productsPerPage, totalProducts)})
+`;
+ }
+
+ categoryLlmsTxt += `
+
+`;
+ }
+
+ if (pageProducts.length > 0) {
+ pageProducts.forEach((product, index) => {
+ if (product.seoName) {
+ // Clean description for markdown (remove HTML tags and limit length)
+ const cleanDescription = product.description
+ ? product.description
+ .replace(/<[^>]*>/g, "")
+ .replace(/\n/g, " ")
+ .trim()
+ .substring(0, 300)
+ : "";
+
+ const globalIndex = startIndex + index + 1;
+ categoryLlmsTxt += `## ${globalIndex}. ${product.name}
+
+- **Product URL**: ${baseUrl}/Artikel/${product.seoName}
+- **Article Number**: ${product.articleNumber || 'N/A'}
+- **Price**: €${product.price || '0.00'}
+- **Brand**: ${product.manufacturer || config.brandName}
+- **Availability**: ${product.available ? 'In Stock' : 'Out of Stock'}`;
+
+ if (product.gtin) {
+ categoryLlmsTxt += `
+- **GTIN**: ${product.gtin}`;
+ }
+
+ if (product.weight && !isNaN(product.weight)) {
+ categoryLlmsTxt += `
+- **Weight**: ${product.weight}g`;
+ }
+
+ if (cleanDescription) {
+ categoryLlmsTxt += `
+
+**Description:**
+${cleanDescription}${product.description && product.description.length > 300 ? '...' : ''}`;
+ }
+
+ categoryLlmsTxt += `
+
+---
+
+`;
+ }
+ });
+ } else {
+ categoryLlmsTxt += `## No Products Available
+
+This category currently contains no products.
+
+`;
+ }
+
+ // Add footer navigation for convenience
+ if (totalPages > 1) {
+ categoryLlmsTxt += `## Page Navigation
+
+`;
+ if (pageNumber > 1) {
+ categoryLlmsTxt += `← [Previous Page](${baseUrl}/llms-${categorySlug}-page-${pageNumber - 1}.txt) | `;
+ }
+
+ categoryLlmsTxt += `[Category Overview](${baseUrl}/llms-${categorySlug}-page-1.txt)`;
+
+ if (pageNumber < totalPages) {
+ categoryLlmsTxt += ` | [Next Page](${baseUrl}/llms-${categorySlug}-page-${pageNumber + 1}.txt) →`;
+ }
+
+ categoryLlmsTxt += `
+
+`;
+ }
+
+ categoryLlmsTxt += `---
+
+*This category product list is automatically generated during the site build process. Product availability and pricing are updated in real-time on the main website.*
+`;
+
+ return categoryLlmsTxt;
+};
+
+// 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,
+ pageNumber,
+ totalPages
+ });
+ }
+
+ return pages;
+};
+
+module.exports = {
+ generateLlmsTxt,
+ generateCategoryLlmsTxt,
+ generateAllCategoryLlmsPages,
+};
\ No newline at end of file
diff --git a/prerender/seo/product.cjs b/prerender/seo/product.cjs
new file mode 100644
index 0000000..33ab23f
--- /dev/null
+++ b/prerender/seo/product.cjs
@@ -0,0 +1,135 @@
+const generateProductMetaTags = (product, baseUrl, config) => {
+ const productUrl = `${baseUrl}/Artikel/${product.seoName}`;
+ const imageUrl =
+ product.pictureList && product.pictureList.trim()
+ ? `${baseUrl}/assets/images/prod${product.pictureList
+ .split(",")[0]
+ .trim()}.jpg`
+ : `${baseUrl}/assets/images/nopicture.jpg`;
+
+ // Clean description for meta (remove HTML tags and limit length)
+ const cleanDescription = product.description
+ ? product.description
+ .replace(/<[^>]*>/g, "")
+ .replace(/\n/g, " ")
+ .substring(0, 160)
+ : `${product.name} - Art.-Nr.: ${product.articleNumber}`;
+
+ return `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${product.gtin ? `` : ''}
+ ${product.articleNumber ? `` : ''}
+ ${product.manufacturer ? `` : ''}
+
+
+
+
+
+
+
+
+
+
+ `;
+};
+
+const generateProductJsonLd = (product, baseUrl, config, categoryInfo = null) => {
+ const productUrl = `${baseUrl}/Artikel/${product.seoName}`;
+ const imageUrl =
+ product.pictureList && product.pictureList.trim()
+ ? `${baseUrl}/assets/images/prod${product.pictureList
+ .split(",")[0]
+ .trim()}.jpg`
+ : `${baseUrl}/assets/images/nopicture.jpg`;
+
+ // Clean description for JSON-LD (remove HTML tags)
+ const cleanDescription = product.description
+ ? product.description.replace(/<[^>]*>/g, "").replace(/\n/g, " ")
+ : product.name;
+
+ // Calculate price valid date (current date + 3 months)
+ const priceValidDate = new Date();
+ priceValidDate.setMonth(priceValidDate.getMonth() + 3);
+
+ const jsonLd = {
+ "@context": "https://schema.org/",
+ "@type": "Product",
+ name: product.name,
+ image: [imageUrl],
+ description: cleanDescription,
+ sku: product.articleNumber,
+ ...(product.gtin && { gtin: product.gtin }),
+ brand: {
+ "@type": "Brand",
+ name: product.manufacturer || "Unknown",
+ },
+ offers: {
+ "@type": "Offer",
+ url: productUrl,
+ priceCurrency: config.currency,
+ price: product.price.toString(),
+ priceValidUntil: priceValidDate.toISOString().split("T")[0],
+ itemCondition: "https://schema.org/NewCondition",
+ availability: product.available
+ ? "https://schema.org/InStock"
+ : "https://schema.org/OutOfStock",
+ seller: {
+ "@type": "Organization",
+ name: config.brandName,
+ },
+ },
+ };
+
+ // Add breadcrumb if category information is available
+ if (categoryInfo && categoryInfo.name && categoryInfo.seoName) {
+ jsonLd.breadcrumb = {
+ "@type": "BreadcrumbList",
+ itemListElement: [
+ {
+ "@type": "ListItem",
+ position: 1,
+ name: "Home",
+ item: baseUrl,
+ },
+ {
+ "@type": "ListItem",
+ position: 2,
+ name: categoryInfo.name,
+ item: `${baseUrl}/Kategorie/${categoryInfo.seoName}`,
+ },
+ {
+ "@type": "ListItem",
+ position: 3,
+ name: product.name,
+ item: productUrl,
+ },
+ ],
+ };
+ }
+
+ return ``;
+};
+
+module.exports = {
+ generateProductMetaTags,
+ generateProductJsonLd,
+};
\ No newline at end of file
diff --git a/prerender/seo/sitemap.cjs b/prerender/seo/sitemap.cjs
new file mode 100644
index 0000000..2737b59
--- /dev/null
+++ b/prerender/seo/sitemap.cjs
@@ -0,0 +1,117 @@
+const generateSitemapJsonLd = (allCategories = [], baseUrl, config) => {
+
+ const jsonLd = {
+ "@context": "https://schema.org/",
+ "@type": "WebPage",
+ name: "Sitemap",
+ url: `${baseUrl}/sitemap`,
+ description: `Sitemap - Übersicht aller Kategorien und Seiten auf ${config.siteName}`,
+ breadcrumb: {
+ "@type": "BreadcrumbList",
+ itemListElement: [
+ {
+ "@type": "ListItem",
+ position: 1,
+ name: "Home",
+ item: baseUrl,
+ },
+ {
+ "@type": "ListItem",
+ position: 2,
+ name: "Sitemap",
+ item: `${baseUrl}/sitemap`,
+ },
+ ],
+ },
+ };
+
+ // Add all categories as site navigation elements
+ if (allCategories && allCategories.length > 0) {
+ jsonLd.mainEntity = {
+ "@type": "SiteNavigationElement",
+ name: "Kategorien",
+ hasPart: allCategories.map((category) => ({
+ "@type": "SiteNavigationElement",
+ name: category.name,
+ url: `${baseUrl}/Kategorie/${category.seoName}`,
+ description: `${category.name} Kategorie`,
+ })),
+ };
+ }
+
+ return ``;
+};
+
+const generateXmlSitemap = (allCategories = [], allProducts = [], baseUrl) => {
+ const currentDate = new Date().toISOString().split("T")[0]; // YYYY-MM-DD format
+
+ let sitemap = `
+
+`;
+
+ // Homepage
+ sitemap += `
+ ${baseUrl}/
+ ${currentDate}
+ daily
+ 1.0
+
+`;
+
+ // Static pages
+ const staticPages = [
+ { path: "/datenschutz", changefreq: "monthly", priority: "0.3" },
+ { path: "/impressum", changefreq: "monthly", priority: "0.3" },
+ { path: "/batteriegesetzhinweise", changefreq: "monthly", priority: "0.3" },
+ { path: "/widerrufsrecht", changefreq: "monthly", priority: "0.3" },
+ { path: "/sitemap", changefreq: "weekly", priority: "0.5" },
+ { path: "/agb", changefreq: "monthly", priority: "0.3" },
+ { path: "/404", changefreq: "monthly", priority: "0.1" },
+ { path: "/Konfigurator", changefreq: "weekly", priority: "0.8" },
+ ];
+
+ staticPages.forEach((page) => {
+ sitemap += `
+ ${baseUrl}${page.path}
+ ${currentDate}
+ ${page.changefreq}
+ ${page.priority}
+
+`;
+ });
+
+ // Category pages
+ allCategories.forEach((category) => {
+ if (category.seoName) {
+ sitemap += `
+ ${baseUrl}/Kategorie/${category.seoName}
+ ${currentDate}
+ weekly
+ 0.8
+
+`;
+ }
+ });
+
+ // Product pages
+ allProducts.forEach((productSeoName) => {
+ sitemap += `
+ ${baseUrl}/Artikel/${productSeoName}
+ ${currentDate}
+ weekly
+ 0.6
+
+`;
+ });
+
+ sitemap += ``;
+
+ return sitemap;
+};
+
+module.exports = {
+ generateSitemapJsonLd,
+ generateXmlSitemap,
+};
\ No newline at end of file