From f8f03b45b8780fb2b49bbf7788be947b49458a7a Mon Sep 17 00:00:00 2001 From: seb Date: Sat, 5 Jul 2025 15:52:34 +0200 Subject: [PATCH] Refactor SEO module to utilize a modular structure by re-exporting all SEO functions from a new index file. This change maintains backward compatibility while streamlining the codebase for future enhancements. --- prerender/seo.cjs | 1195 +------------------------------- prerender/seo/category.cjs | 81 +++ prerender/seo/feeds.cjs | 344 +++++++++ prerender/seo/homepage.cjs | 215 ++++++ prerender/seo/index.cjs | 64 ++ prerender/seo/konfigurator.cjs | 36 + prerender/seo/llms.cjs | 277 ++++++++ prerender/seo/product.cjs | 135 ++++ prerender/seo/sitemap.cjs | 117 ++++ 9 files changed, 1272 insertions(+), 1192 deletions(-) create mode 100644 prerender/seo/category.cjs create mode 100644 prerender/seo/feeds.cjs create mode 100644 prerender/seo/homepage.cjs create mode 100644 prerender/seo/index.cjs create mode 100644 prerender/seo/konfigurator.cjs create mode 100644 prerender/seo/llms.cjs create mode 100644 prerender/seo/product.cjs create mode 100644 prerender/seo/sitemap.cjs 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