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 = {