From 21d86565f158fb7faa680bfe1114be7284e51a02 Mon Sep 17 00:00:00 2001 From: sebseb7 Date: Sat, 28 Mar 2026 17:10:14 +0100 Subject: [PATCH] refactor: Enhance JSON-LD structure in category and product generation functions for improved SEO and consistency across URLs --- prerender/seo/category.cjs | 117 +++++++++---- prerender/seo/homepage.cjs | 325 ++++++++++++++++++++----------------- prerender/seo/product.cjs | 232 +++++++++++++++----------- 3 files changed, 396 insertions(+), 278 deletions(-) diff --git a/prerender/seo/category.cjs b/prerender/seo/category.cjs index a45390e..edd5f0b 100644 --- a/prerender/seo/category.cjs +++ b/prerender/seo/category.cjs @@ -7,41 +7,82 @@ const generateCategoryJsonLd = (category, products = [], baseUrl, config) => { return ''; } - const categoryUrl = `${baseUrl}/Kategorie/${category.seoName}`; + const root = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl; + const categoryUrl = `${root}/Kategorie/${category.seoName}`; // Calculate price valid date (current date + 3 months) const priceValidDate = new Date(); priceValidDate.setMonth(priceValidDate.getMonth() + 3); const priceValidUntil = priceValidDate.toISOString().split("T")[0]; - const jsonLd = { - "@context": "https://schema.org/", + const id = { + business: `${root}#business`, + website: `${root}#website`, + breadcrumb: `${categoryUrl}#breadcrumb`, + itemList: `${categoryUrl}#itemlist`, + }; + + const logoUrl = + config.images && config.images.logo + ? `${root}${config.images.logo}` + : undefined; + + const businessNode = { + "@id": id.business, + "@type": ["GardenStore", "LocalBusiness", "Organization"], + name: config.brandName, + url: root, + ...(logoUrl && { + logo: { "@type": "ImageObject", url: logoUrl }, + image: { "@type": "ImageObject", url: logoUrl }, + }), + }; + + const websiteNode = { + "@id": id.website, + "@type": "WebSite", + name: config.siteName || config.brandName, + url: root, + publisher: { "@id": id.business }, + }; + + const breadcrumbNode = { + "@id": id.breadcrumb, + "@type": "BreadcrumbList", + itemListElement: [ + { + "@type": "ListItem", + position: 1, + name: "Home", + item: root, + }, + { + "@type": "ListItem", + position: 2, + name: category.name, + item: categoryUrl, + }, + ], + }; + + const collectionPageNode = { + "@id": categoryUrl, "@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, - }, - ], - }, + isPartOf: { "@id": id.website }, + breadcrumb: { "@id": id.breadcrumb }, }; + const graph = [businessNode, websiteNode, breadcrumbNode, collectionPageNode]; + // Add product list if products are available if (products && products.length > 0) { - jsonLd.mainEntity = { + collectionPageNode.mainEntity = { "@id": id.itemList }; + + graph.push({ + "@id": id.itemList, "@type": "ItemList", numberOfItems: products.length, itemListElement: products.slice(0, 20).map((product, index) => ({ @@ -50,14 +91,14 @@ const generateCategoryJsonLd = (category, products = [], baseUrl, config) => { item: { "@type": "Product", name: product.name, - url: `${baseUrl}/Artikel/${product.seoName}`, + url: `${root}/Artikel/${product.seoName}`, image: product.pictureList && product.pictureList.trim() - ? `${baseUrl}/assets/images/prod${product.pictureList + ? `${root}/assets/images/prod${product.pictureList .split(",")[0] .trim()}.avif` - : `${baseUrl}/assets/images/nopicture.jpg`, - description: product.description + : `${root}/assets/images/nopicture.jpg`, + description: product.description ? product.description.replace(/<[^>]*>/g, "").substring(0, 200) : `${product.name} - Hochwertiges Growshop Produkt`, sku: product.articleNumber || product.seoName, @@ -67,22 +108,23 @@ const generateCategoryJsonLd = (category, products = [], baseUrl, config) => { }, offers: { "@type": "Offer", - url: `${baseUrl}/Artikel/${product.seoName}`, - price: product.price && !isNaN(product.price) ? product.price.toString() : "0.00", + url: `${root}/Artikel/${product.seoName}`, + price: + product.price && !isNaN(product.price) + ? product.price.toString() + : "0.00", priceCurrency: config.currency, priceValidUntil: priceValidUntil, availability: product.available ? "https://schema.org/InStock" : "https://schema.org/OutOfStock", - seller: { - "@type": "Organization", - name: config.brandName, - }, + seller: { "@id": id.business }, itemCondition: "https://schema.org/NewCondition", hasMerchantReturnPolicy: { "@type": "MerchantReturnPolicy", applicableCountry: "DE", - returnPolicyCategory: "https://schema.org/MerchantReturnFiniteReturnWindow", + returnPolicyCategory: + "https://schema.org/MerchantReturnFiniteReturnWindow", merchantReturnDays: 14, returnMethod: "https://schema.org/ReturnByMail", returnFees: "https://schema.org/FreeReturn", @@ -91,7 +133,7 @@ const generateCategoryJsonLd = (category, products = [], baseUrl, config) => { "@type": "OfferShippingDetails", shippingRate: { "@type": "MonetaryAmount", - value: 5.90, + value: 5.9, currency: "EUR", }, shippingDestination: { @@ -117,11 +159,16 @@ const generateCategoryJsonLd = (category, products = [], baseUrl, config) => { }, }, })), - }; + }); } + const categoryGraph = { + "@context": "https://schema.org", + "@graph": graph, + }; + return ``; }; diff --git a/prerender/seo/homepage.cjs b/prerender/seo/homepage.cjs index 97173eb..2a79206 100644 --- a/prerender/seo/homepage.cjs +++ b/prerender/seo/homepage.cjs @@ -36,177 +36,198 @@ const generateHomepageJsonLd = (baseUrl, config, categories = []) => { const canonicalUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl; const logoUrl = `${canonicalUrl}${config.images.logo}`; - const websiteJsonLd = { - "@context": "https://schema.org/", + const id = { + business: `${canonicalUrl}#business`, + website: `${canonicalUrl}#website`, + faq: `${canonicalUrl}#faq`, + categoryList: `${canonicalUrl}#category-list`, + sitemapPage: `${canonicalUrl}/sitemap#webpage`, + }; + + const organizationNode = { + "@id": id.business, + "@type": ["GardenStore", "LocalBusiness", "Organization"], + name: config.brandName, + alternateName: config.siteName, + description: config.descriptions.de.long, + url: canonicalUrl, + logo: { + "@type": "ImageObject", + url: logoUrl, + }, + image: { + "@type": "ImageObject", + url: 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: [], + }; + + const sitemapWebPageNode = { + "@id": id.sitemapPage, + "@type": "WebPage", + name: "Sitemap", + url: `${canonicalUrl}/sitemap`, + description: "Vollständige Sitemap mit allen Kategorien und Seiten", + isPartOf: { "@id": id.website }, + }; + + const websiteNode = { + "@id": id.website, "@type": "WebSite", name: config.brandName, url: canonicalUrl, description: config.descriptions.de.long, - publisher: { - "@type": "Organization", - name: config.brandName, - url: canonicalUrl, - logo: { - "@type": "ImageObject", - url: logoUrl, - }, - }, + publisher: { "@id": id.business }, potentialAction: { "@type": "SearchAction", target: `${canonicalUrl}/search?q={search_term_string}`, - query: "required name=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 - ], + mainEntity: { "@id": id.sitemapPage }, + sameAs: [], }; - // Store entity: specific subtype (GardenStore) + LocalBusiness + Organization for clear merchant categorization - const organizationJsonLd = { - "@context": "https://schema.org", - "@type": ["GardenStore", "LocalBusiness", "Organization"], - "name": config.brandName, - "alternateName": config.siteName, - "description": config.descriptions.de.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" - } + const faqMainEntity = [ + { + "@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": "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" - ] - }; + }, + { + "@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.", + }, + }, + ]; - // FAQPage Schema for common questions - const faqJsonLd = { - "@context": "https://schema.org", + const faqNode = { + "@id": id.faq, "@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." - } - } - ] + url: canonicalUrl, + publisher: { "@id": id.business }, + isPartOf: { "@id": id.website }, + mainEntity: faqMainEntity, }; - // 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) => ({ + const filteredCategories = categories.filter((c) => c.seoName); + + const graph = [ + organizationNode, + websiteNode, + sitemapWebPageNode, + faqNode, + ]; + + if (filteredCategories.length > 0) { + graph.push({ + "@id": id.categoryList, + "@type": "ItemList", + name: "Produktkategorien", + description: "Alle verfügbaren Produktkategorien in unserem Online-Shop", + numberOfItems: filteredCategories.length, + isPartOf: { "@id": id.website }, + itemListElement: filteredCategories.map((category, index) => ({ "@type": "ListItem", - "position": index + 1, - "item": { + position: index + 1, + item: { "@type": "Thing", - "name": category.name, - "url": `${canonicalUrl}/Kategorie/${category.seoName}` - } - })) + name: category.name, + url: `${canonicalUrl}/Kategorie/${category.seoName}`, + }, + })), + }); + } + + const homepageGraph = { + "@context": "https://schema.org", + "@graph": graph, }; - // 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 : ''); + return ``; }; module.exports = { diff --git a/prerender/seo/product.cjs b/prerender/seo/product.cjs index 0ef59f5..d116bb4 100644 --- a/prerender/seo/product.cjs +++ b/prerender/seo/product.cjs @@ -68,14 +68,15 @@ const generateProductMetaTags = (product, baseUrl, config) => { }; const generateProductJsonLd = (product, baseUrl, config, categoryInfo = null) => { - const productUrl = `${baseUrl}/Artikel/${product.seoName}`; + const root = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl; + const productUrl = `${root}/Artikel/${product.seoName}`; const pictureFirstId = product.pictureList && product.pictureList.trim() ? product.pictureList.split(",")[0].trim() : null; const imageUrl = pictureFirstId - ? `${baseUrl}/assets/images/prod${pictureFirstId}.avif` - : `${baseUrl}/assets/images/nopicture.jpg`; + ? `${root}/assets/images/prod${pictureFirstId}.avif` + : `${root}/assets/images/nopicture.jpg`; // Clean description for JSON-LD (remove HTML tags) const cleanDescription = product.description @@ -86,8 +87,87 @@ const generateProductJsonLd = (product, baseUrl, config, categoryInfo = null) => const priceValidDate = new Date(); priceValidDate.setMonth(priceValidDate.getMonth() + 3); - const productJsonLd = { - "@context": "https://schema.org/", + const id = { + business: `${root}#business`, + website: `${root}#website`, + product: `${productUrl}#product`, + breadcrumb: `${productUrl}#breadcrumb`, + }; + + const logoUrl = + config.images && config.images.logo + ? `${root}${config.images.logo}` + : undefined; + + const businessNode = { + "@id": id.business, + "@type": ["GardenStore", "LocalBusiness", "Organization"], + name: config.brandName, + url: root, + ...(logoUrl && { + logo: { "@type": "ImageObject", url: logoUrl }, + image: { "@type": "ImageObject", url: logoUrl }, + }), + }; + + const websiteNode = { + "@id": id.website, + "@type": "WebSite", + name: config.siteName || config.brandName, + url: root, + publisher: { "@id": id.business }, + }; + + const offer = { + "@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: { "@id": id.business }, + hasMerchantReturnPolicy: { + "@type": "MerchantReturnPolicy", + applicableCountry: "DE", + returnPolicyCategory: "https://schema.org/MerchantReturnFiniteReturnWindow", + merchantReturnDays: 14, + returnMethod: "https://schema.org/ReturnByMail", + returnFees: "https://schema.org/FreeReturn", + }, + shippingDetails: { + "@type": "OfferShippingDetails", + shippingRate: { + "@type": "MonetaryAmount", + value: 5.9, + currency: "EUR", + }, + shippingDestination: { + "@type": "DefinedRegion", + addressCountry: "DE", + }, + deliveryTime: { + "@type": "ShippingDeliveryTime", + handlingTime: { + "@type": "QuantitativeValue", + minValue: 0, + maxValue: 1, + unitCode: "DAY", + }, + transitTime: { + "@type": "QuantitativeValue", + minValue: 2, + maxValue: 3, + unitCode: "DAY", + }, + }, + }, + }; + + const productNode = { + "@id": id.product, "@type": "Product", name: product.name, image: [imageUrl], @@ -98,95 +178,65 @@ const generateProductJsonLd = (product, baseUrl, config, categoryInfo = null) => "@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, - }, - hasMerchantReturnPolicy: { - "@type": "MerchantReturnPolicy", - applicableCountry: "DE", - returnPolicyCategory: "https://schema.org/MerchantReturnFiniteReturnWindow", - merchantReturnDays: 14, - returnMethod: "https://schema.org/ReturnByMail", - returnFees: "https://schema.org/FreeReturn", - }, - shippingDetails: { - "@type": "OfferShippingDetails", - shippingRate: { - "@type": "MonetaryAmount", - value: 5.90, - currency: "EUR", - }, - shippingDestination: { - "@type": "DefinedRegion", - addressCountry: "DE", - }, - deliveryTime: { - "@type": "ShippingDeliveryTime", - handlingTime: { - "@type": "QuantitativeValue", - minValue: 0, - maxValue: 1, - unitCode: "DAY", - }, - transitTime: { - "@type": "QuantitativeValue", - minValue: 2, - maxValue: 3, - unitCode: "DAY", - }, - }, - }, - }, + offers: offer, }; - const productScript = ``; - - // BreadcrumbList is not a valid property on Product; emit as its own JSON-LD block (WebPage path context). - if (categoryInfo && categoryInfo.name && categoryInfo.seoName) { - const breadcrumbJsonLd = { - "@context": "https://schema.org/", - "@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, - }, - ], - }; - const breadcrumbScript = ``; - return `${productScript}\n${breadcrumbScript}`; - } - - return productScript; }; module.exports = {