Compare commits

...

2 Commits

3 changed files with 396 additions and 278 deletions

View File

@@ -7,27 +7,54 @@ const generateCategoryJsonLd = (category, products = [], baseUrl, config) => {
return ''; 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) // Calculate price valid date (current date + 3 months)
const priceValidDate = new Date(); const priceValidDate = new Date();
priceValidDate.setMonth(priceValidDate.getMonth() + 3); priceValidDate.setMonth(priceValidDate.getMonth() + 3);
const priceValidUntil = priceValidDate.toISOString().split("T")[0]; const priceValidUntil = priceValidDate.toISOString().split("T")[0];
const jsonLd = { const id = {
"@context": "https://schema.org/", business: `${root}#business`,
"@type": "CollectionPage", website: `${root}#website`,
name: category.name, breadcrumb: `${categoryUrl}#breadcrumb`,
url: categoryUrl, itemList: `${categoryUrl}#itemlist`,
description: `${category.name} - Entdecken Sie unsere Auswahl an hochwertigen Produkten`, };
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 breadcrumbNode = {
"@id": id.breadcrumb,
"@type": "BreadcrumbList", "@type": "BreadcrumbList",
itemListElement: [ itemListElement: [
{ {
"@type": "ListItem", "@type": "ListItem",
position: 1, position: 1,
name: "Home", name: "Home",
item: baseUrl, item: root,
}, },
{ {
"@type": "ListItem", "@type": "ListItem",
@@ -36,12 +63,26 @@ const generateCategoryJsonLd = (category, products = [], baseUrl, config) => {
item: categoryUrl, item: categoryUrl,
}, },
], ],
},
}; };
const collectionPageNode = {
"@id": categoryUrl,
"@type": "CollectionPage",
name: category.name,
url: categoryUrl,
description: `${category.name} - Entdecken Sie unsere Auswahl an hochwertigen Produkten`,
isPartOf: { "@id": id.website },
breadcrumb: { "@id": id.breadcrumb },
};
const graph = [businessNode, websiteNode, breadcrumbNode, collectionPageNode];
// Add product list if products are available // Add product list if products are available
if (products && products.length > 0) { if (products && products.length > 0) {
jsonLd.mainEntity = { collectionPageNode.mainEntity = { "@id": id.itemList };
graph.push({
"@id": id.itemList,
"@type": "ItemList", "@type": "ItemList",
numberOfItems: products.length, numberOfItems: products.length,
itemListElement: products.slice(0, 20).map((product, index) => ({ itemListElement: products.slice(0, 20).map((product, index) => ({
@@ -50,13 +91,13 @@ const generateCategoryJsonLd = (category, products = [], baseUrl, config) => {
item: { item: {
"@type": "Product", "@type": "Product",
name: product.name, name: product.name,
url: `${baseUrl}/Artikel/${product.seoName}`, url: `${root}/Artikel/${product.seoName}`,
image: image:
product.pictureList && product.pictureList.trim() product.pictureList && product.pictureList.trim()
? `${baseUrl}/assets/images/prod${product.pictureList ? `${root}/assets/images/prod${product.pictureList
.split(",")[0] .split(",")[0]
.trim()}.avif` .trim()}.avif`
: `${baseUrl}/assets/images/nopicture.jpg`, : `${root}/assets/images/nopicture.jpg`,
description: product.description description: product.description
? product.description.replace(/<[^>]*>/g, "").substring(0, 200) ? product.description.replace(/<[^>]*>/g, "").substring(0, 200)
: `${product.name} - Hochwertiges Growshop Produkt`, : `${product.name} - Hochwertiges Growshop Produkt`,
@@ -67,22 +108,23 @@ const generateCategoryJsonLd = (category, products = [], baseUrl, config) => {
}, },
offers: { offers: {
"@type": "Offer", "@type": "Offer",
url: `${baseUrl}/Artikel/${product.seoName}`, url: `${root}/Artikel/${product.seoName}`,
price: product.price && !isNaN(product.price) ? product.price.toString() : "0.00", price:
product.price && !isNaN(product.price)
? product.price.toString()
: "0.00",
priceCurrency: config.currency, priceCurrency: config.currency,
priceValidUntil: priceValidUntil, priceValidUntil: priceValidUntil,
availability: product.available availability: product.available
? "https://schema.org/InStock" ? "https://schema.org/InStock"
: "https://schema.org/OutOfStock", : "https://schema.org/OutOfStock",
seller: { seller: { "@id": id.business },
"@type": "Organization",
name: config.brandName,
},
itemCondition: "https://schema.org/NewCondition", itemCondition: "https://schema.org/NewCondition",
hasMerchantReturnPolicy: { hasMerchantReturnPolicy: {
"@type": "MerchantReturnPolicy", "@type": "MerchantReturnPolicy",
applicableCountry: "DE", applicableCountry: "DE",
returnPolicyCategory: "https://schema.org/MerchantReturnFiniteReturnWindow", returnPolicyCategory:
"https://schema.org/MerchantReturnFiniteReturnWindow",
merchantReturnDays: 14, merchantReturnDays: 14,
returnMethod: "https://schema.org/ReturnByMail", returnMethod: "https://schema.org/ReturnByMail",
returnFees: "https://schema.org/FreeReturn", returnFees: "https://schema.org/FreeReturn",
@@ -91,7 +133,7 @@ const generateCategoryJsonLd = (category, products = [], baseUrl, config) => {
"@type": "OfferShippingDetails", "@type": "OfferShippingDetails",
shippingRate: { shippingRate: {
"@type": "MonetaryAmount", "@type": "MonetaryAmount",
value: 5.90, value: 5.9,
currency: "EUR", currency: "EUR",
}, },
shippingDestination: { shippingDestination: {
@@ -117,11 +159,16 @@ const generateCategoryJsonLd = (category, products = [], baseUrl, config) => {
}, },
}, },
})), })),
}; });
} }
const categoryGraph = {
"@context": "https://schema.org",
"@graph": graph,
};
return `<script type="application/ld+json">${JSON.stringify( return `<script type="application/ld+json">${JSON.stringify(
jsonLd categoryGraph
)}</script>`; )}</script>`;
}; };

View File

@@ -36,177 +36,198 @@ const generateHomepageJsonLd = (baseUrl, config, categories = []) => {
const canonicalUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl; const canonicalUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
const logoUrl = `${canonicalUrl}${config.images.logo}`; const logoUrl = `${canonicalUrl}${config.images.logo}`;
const websiteJsonLd = { const id = {
"@context": "https://schema.org/", business: `${canonicalUrl}#business`,
"@type": "WebSite", 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, name: config.brandName,
url: canonicalUrl, alternateName: config.siteName,
description: config.descriptions.de.long, description: config.descriptions.de.long,
publisher: {
"@type": "Organization",
name: config.brandName,
url: canonicalUrl, url: canonicalUrl,
logo: { logo: {
"@type": "ImageObject", "@type": "ImageObject",
url: logoUrl, url: logoUrl,
}, },
image: {
"@type": "ImageObject",
url: logoUrl,
}, },
potentialAction: { telephone: "015208491860",
"@type": "SearchAction", email: "service@growheads.de",
target: `${canonicalUrl}/search?q={search_term_string}`, address: {
query: "required name=search_term_string" "@type": "PostalAddress",
streetAddress: "Trachenberger Strasse 14",
addressLocality: "Dresden",
postalCode: "01129",
addressCountry: "DE",
addressRegion: "Sachsen",
}, },
mainEntity: { 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", "@type": "WebPage",
name: "Sitemap", name: "Sitemap",
url: `${canonicalUrl}/sitemap`, url: `${canonicalUrl}/sitemap`,
description: "Vollständige Sitemap mit allen Kategorien und Seiten", description: "Vollständige Sitemap mit allen Kategorien und Seiten",
}, isPartOf: { "@id": id.website },
sameAs: [
// Add your social media URLs here if available
],
}; };
// Organization/LocalBusiness Schema for rich results const websiteNode = {
const organizationJsonLd = { "@id": id.website,
"@context": "https://schema.org", "@type": "WebSite",
"@type": "LocalBusiness", name: config.brandName,
"name": config.brandName, url: canonicalUrl,
"alternateName": config.siteName, description: config.descriptions.de.long,
"description": config.descriptions.de.long, publisher: { "@id": id.business },
"url": canonicalUrl, potentialAction: {
"logo": logoUrl, "@type": "SearchAction",
"image": logoUrl, target: `${canonicalUrl}/search?q={search_term_string}`,
"telephone": "015208491860", query: "required name=search_term_string",
"email": "service@growheads.de",
"address": {
"@type": "PostalAddress",
"streetAddress": "Trachenberger Strasse 14",
"addressLocality": "Dresden",
"postalCode": "01129",
"addressCountry": "DE",
"addressRegion": "Sachsen"
}, },
"geo": { mainEntity: { "@id": id.sitemapPage },
"@type": "GeoCoordinates", sameAs: [],
"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 faqMainEntity = [
const faqJsonLd = { {
"@context": "https://schema.org", "@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.",
},
},
];
const faqNode = {
"@id": id.faq,
"@type": "FAQPage", "@type": "FAQPage",
"mainEntity": [ url: canonicalUrl,
{ publisher: { "@id": id.business },
"@type": "Question", isPartOf: { "@id": id.website },
"name": "Welche Zahlungsmethoden akzeptiert GrowHeads?", mainEntity: faqMainEntity,
"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 filteredCategories = categories.filter((c) => c.seoName);
const categoriesListJsonLd = {
"@context": "https://schema.org", const graph = [
organizationNode,
websiteNode,
sitemapWebPageNode,
faqNode,
];
if (filteredCategories.length > 0) {
graph.push({
"@id": id.categoryList,
"@type": "ItemList", "@type": "ItemList",
"name": "Produktkategorien", name: "Produktkategorien",
"description": "Alle verfügbaren Produktkategorien in unserem Online-Shop", description: "Alle verfügbaren Produktkategorien in unserem Online-Shop",
"numberOfItems": categories.filter(category => category.seoName).length, numberOfItems: filteredCategories.length,
"itemListElement": categories isPartOf: { "@id": id.website },
.filter(category => category.seoName) // Only include categories with seoName itemListElement: filteredCategories.map((category, index) => ({
.map((category, index) => ({
"@type": "ListItem", "@type": "ListItem",
"position": index + 1, position: index + 1,
"item": { item: {
"@type": "Thing", "@type": "Thing",
"name": category.name, name: category.name,
"url": `${canonicalUrl}/Kategorie/${category.seoName}` url: `${canonicalUrl}/Kategorie/${category.seoName}`,
},
})),
});
} }
}))
const homepageGraph = {
"@context": "https://schema.org",
"@graph": graph,
}; };
// Return all JSON-LD scripts return `<script type="application/ld+json">${JSON.stringify(
const websiteScript = `<script type="application/ld+json">${JSON.stringify(websiteJsonLd)}</script>`; homepageGraph
const organizationScript = `<script type="application/ld+json">${JSON.stringify(organizationJsonLd)}</script>`; )}</script>`;
const faqScript = `<script type="application/ld+json">${JSON.stringify(faqJsonLd)}</script>`;
const categoriesScript = categories.length > 0
? `<script type="application/ld+json">${JSON.stringify(categoriesListJsonLd)}</script>`
: '';
return websiteScript + '\n' + organizationScript + '\n' + faqScript + (categoriesScript ? '\n' + categoriesScript : '');
}; };
module.exports = { module.exports = {

View File

@@ -68,14 +68,15 @@ const generateProductMetaTags = (product, baseUrl, config) => {
}; };
const generateProductJsonLd = (product, baseUrl, config, categoryInfo = null) => { 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 = const pictureFirstId =
product.pictureList && product.pictureList.trim() product.pictureList && product.pictureList.trim()
? product.pictureList.split(",")[0].trim() ? product.pictureList.split(",")[0].trim()
: null; : null;
const imageUrl = pictureFirstId const imageUrl = pictureFirstId
? `${baseUrl}/assets/images/prod${pictureFirstId}.avif` ? `${root}/assets/images/prod${pictureFirstId}.avif`
: `${baseUrl}/assets/images/nopicture.jpg`; : `${root}/assets/images/nopicture.jpg`;
// Clean description for JSON-LD (remove HTML tags) // Clean description for JSON-LD (remove HTML tags)
const cleanDescription = product.description const cleanDescription = product.description
@@ -86,19 +87,38 @@ const generateProductJsonLd = (product, baseUrl, config, categoryInfo = null) =>
const priceValidDate = new Date(); const priceValidDate = new Date();
priceValidDate.setMonth(priceValidDate.getMonth() + 3); priceValidDate.setMonth(priceValidDate.getMonth() + 3);
const productJsonLd = { const id = {
"@context": "https://schema.org/", business: `${root}#business`,
"@type": "Product", website: `${root}#website`,
name: product.name, product: `${productUrl}#product`,
image: [imageUrl], breadcrumb: `${productUrl}#breadcrumb`,
description: cleanDescription, };
sku: product.articleNumber,
...(product.gtin && { gtin: product.gtin }), const logoUrl =
brand: { config.images && config.images.logo
"@type": "Brand", ? `${root}${config.images.logo}`
name: product.manufacturer || "Unknown", : undefined;
},
offers: { 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", "@type": "Offer",
url: productUrl, url: productUrl,
priceCurrency: config.currency, priceCurrency: config.currency,
@@ -108,10 +128,7 @@ const generateProductJsonLd = (product, baseUrl, config, categoryInfo = null) =>
availability: product.available availability: product.available
? "https://schema.org/InStock" ? "https://schema.org/InStock"
: "https://schema.org/OutOfStock", : "https://schema.org/OutOfStock",
seller: { seller: { "@id": id.business },
"@type": "Organization",
name: config.brandName,
},
hasMerchantReturnPolicy: { hasMerchantReturnPolicy: {
"@type": "MerchantReturnPolicy", "@type": "MerchantReturnPolicy",
applicableCountry: "DE", applicableCountry: "DE",
@@ -124,7 +141,7 @@ const generateProductJsonLd = (product, baseUrl, config, categoryInfo = null) =>
"@type": "OfferShippingDetails", "@type": "OfferShippingDetails",
shippingRate: { shippingRate: {
"@type": "MonetaryAmount", "@type": "MonetaryAmount",
value: 5.90, value: 5.9,
currency: "EUR", currency: "EUR",
}, },
shippingDestination: { shippingDestination: {
@@ -147,30 +164,42 @@ const generateProductJsonLd = (product, baseUrl, config, categoryInfo = null) =>
}, },
}, },
}, },
},
}; };
const productScript = `<script type="application/ld+json">${JSON.stringify( const productNode = {
productJsonLd "@id": id.product,
)}</script>`; "@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: offer,
};
// BreadcrumbList is not a valid property on Product; emit as its own JSON-LD block (WebPage path context). const hasBreadcrumb =
if (categoryInfo && categoryInfo.name && categoryInfo.seoName) { categoryInfo && categoryInfo.name && categoryInfo.seoName;
const breadcrumbJsonLd = {
"@context": "https://schema.org/", const breadcrumbList = hasBreadcrumb
? {
"@id": id.breadcrumb,
"@type": "BreadcrumbList", "@type": "BreadcrumbList",
itemListElement: [ itemListElement: [
{ {
"@type": "ListItem", "@type": "ListItem",
position: 1, position: 1,
name: "Home", name: "Home",
item: baseUrl, item: root,
}, },
{ {
"@type": "ListItem", "@type": "ListItem",
position: 2, position: 2,
name: categoryInfo.name, name: categoryInfo.name,
item: `${baseUrl}/Kategorie/${categoryInfo.seoName}`, item: `${root}/Kategorie/${categoryInfo.seoName}`,
}, },
{ {
"@type": "ListItem", "@type": "ListItem",
@@ -179,14 +208,35 @@ const generateProductJsonLd = (product, baseUrl, config, categoryInfo = null) =>
item: productUrl, item: productUrl,
}, },
], ],
};
const breadcrumbScript = `<script type="application/ld+json">${JSON.stringify(
breadcrumbJsonLd
)}</script>`;
return `${productScript}\n${breadcrumbScript}`;
} }
: null;
return productScript; const itemPageNode = {
"@id": productUrl,
"@type": "ItemPage",
url: productUrl,
name: product.name,
isPartOf: { "@id": id.website },
mainEntity: { "@id": id.product },
...(hasBreadcrumb && { breadcrumb: { "@id": id.breadcrumb } }),
};
const graph = [
businessNode,
websiteNode,
itemPageNode,
...(breadcrumbList ? [breadcrumbList] : []),
productNode,
];
const productGraph = {
"@context": "https://schema.org",
"@graph": graph,
};
return `<script type="application/ld+json">${JSON.stringify(
productGraph
)}</script>`;
}; };
module.exports = { module.exports = {