From fd0490c3e1a4729341f8d7deea8f760c9dcdece8 Mon Sep 17 00:00:00 2001 From: Unchained Date: Mon, 30 Mar 2026 11:42:58 +0200 Subject: [PATCH] feat: integrate SEO system into pages - Add OrganizationSchema to root layout - Add ProductSchema with metadata to product pages - Add enhanced metadata to homepage with keywords - Add enhanced metadata to products listing page - Add noindex to checkout page via layout - Implement canonical URLs, OpenGraph, and Twitter cards --- src/app/[locale]/checkout/layout.tsx | 26 ++++++++++ src/app/[locale]/page.tsx | 36 +++++++++++++- src/app/[locale]/products/[slug]/page.tsx | 59 ++++++++++++++++++++++- src/app/[locale]/products/page.tsx | 29 ++++++++++- src/app/layout.tsx | 7 +++ 5 files changed, 154 insertions(+), 3 deletions(-) create mode 100644 src/app/[locale]/checkout/layout.tsx diff --git a/src/app/[locale]/checkout/layout.tsx b/src/app/[locale]/checkout/layout.tsx new file mode 100644 index 0000000..396bfb9 --- /dev/null +++ b/src/app/[locale]/checkout/layout.tsx @@ -0,0 +1,26 @@ +import type { Metadata } from "next"; +import { getPageKeywords } from "@/lib/seo/keywords"; +import { isValidLocale, DEFAULT_LOCALE } from "@/lib/i18n/locales"; + +export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise { + const { locale } = await params; + const validLocale = isValidLocale(locale) ? locale : DEFAULT_LOCALE; + const keywords = getPageKeywords(validLocale, 'checkout'); + + return { + title: keywords.metaTitle, + description: keywords.metaDescription, + robots: { + index: false, + follow: false, + }, + }; +} + +export default function CheckoutLayout({ + children, +}: { + children: React.ReactNode; +}) { + return children; +} diff --git a/src/app/[locale]/page.tsx b/src/app/[locale]/page.tsx index 043cc96..1e85852 100644 --- a/src/app/[locale]/page.tsx +++ b/src/app/[locale]/page.tsx @@ -12,15 +12,49 @@ import ProblemSection from "@/components/home/ProblemSection"; import HowItWorks from "@/components/home/HowItWorks"; import { getPageMetadata } from "@/lib/i18n/pageMetadata"; import { isValidLocale, DEFAULT_LOCALE, getSaleorLocale, type Locale } from "@/lib/i18n/locales"; +import { getPageKeywords, getBrandKeywords } from "@/lib/seo/keywords"; +import { Metadata } from "next"; -export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) { +const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://dev.manoonoils.com"; + +export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise { const { locale } = await params; const validLocale = isValidLocale(locale) ? locale : DEFAULT_LOCALE; const metadata = getPageMetadata(validLocale as Locale); + const keywords = getPageKeywords(validLocale as Locale, 'home'); + const brand = getBrandKeywords(validLocale as Locale); setRequestLocale(validLocale); + + // Build canonical URL + const localePrefix = validLocale === DEFAULT_LOCALE ? "" : `/${validLocale}`; + const canonicalUrl = `${baseUrl}${localePrefix || '/'}`; + return { title: metadata.home.title, description: metadata.home.description, + keywords: [...keywords.primary, ...keywords.secondary].join(', '), + alternates: { + canonical: canonicalUrl, + }, + openGraph: { + title: metadata.home.title, + description: metadata.home.description, + type: 'website', + url: canonicalUrl, + images: [{ + url: `${baseUrl}/og-image.jpg`, + width: 1200, + height: 630, + alt: brand.tagline, + }], + locale: validLocale, + }, + twitter: { + card: 'summary_large_image', + title: metadata.home.title, + description: metadata.home.description, + images: [`${baseUrl}/og-image.jpg`], + }, }; } diff --git a/src/app/[locale]/products/[slug]/page.tsx b/src/app/[locale]/products/[slug]/page.tsx index 96b7819..e973a3c 100644 --- a/src/app/[locale]/products/[slug]/page.tsx +++ b/src/app/[locale]/products/[slug]/page.tsx @@ -7,6 +7,9 @@ import type { Product } from "@/types/saleor"; import { routing } from "@/i18n/routing"; import { getPageMetadata } from "@/lib/i18n/pageMetadata"; import { isValidLocale, DEFAULT_LOCALE, getSaleorLocale, type Locale } from "@/lib/i18n/locales"; +import { ProductSchema } from "@/components/seo"; +import { getPageKeywords } from "@/lib/seo/keywords"; +import { Metadata } from "next"; interface ProductPageProps { params: Promise<{ locale: string; slug: string }>; @@ -30,7 +33,9 @@ export async function generateStaticParams() { return params; } -export async function generateMetadata({ params }: ProductPageProps) { +const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://dev.manoonoils.com"; + +export async function generateMetadata({ params }: ProductPageProps): Promise { const { locale, slug } = await params; const validLocale = isValidLocale(locale) ? locale : DEFAULT_LOCALE; const metadata = getPageMetadata(validLocale as Locale); @@ -44,10 +49,41 @@ export async function generateMetadata({ params }: ProductPageProps) { } const localized = getLocalizedProduct(product, saleorLocale); + const keywords = getPageKeywords(validLocale as Locale, 'product'); + + // Build canonical URL + const localePrefix = validLocale === DEFAULT_LOCALE ? "" : `/${validLocale}`; + const canonicalUrl = `${baseUrl}${localePrefix}/products/${slug}`; + + // Get product image for OpenGraph + const productImage = product.media?.[0]?.url || `${baseUrl}/og-image.jpg`; return { title: localized.name, description: localized.seoDescription || localized.description?.slice(0, 160), + keywords: [...keywords.primary, ...keywords.secondary].join(', '), + alternates: { + canonical: canonicalUrl, + }, + openGraph: { + title: localized.name, + description: localized.seoDescription || localized.description?.slice(0, 160), + type: 'website', + url: canonicalUrl, + images: [{ + url: productImage, + width: 1200, + height: 630, + alt: localized.name, + }], + locale: validLocale, + }, + twitter: { + card: 'summary_large_image', + title: localized.name, + description: localized.seoDescription || localized.description?.slice(0, 160), + images: [productImage], + }, }; } @@ -108,8 +144,29 @@ export default async function ProductPage({ params }: ProductPageProps) { }); } catch (e) {} + // Prepare product data for schema + const firstVariant = product.variants?.[0]; + const productSchemaData = { + name: product.name, + slug: product.slug, + description: product.description || product.name, + images: product.media?.map(m => m.url) || [`${baseUrl}/og-image.jpg`], + price: { + amount: firstVariant?.pricing?.price?.gross?.amount || 0, + currency: firstVariant?.pricing?.price?.gross?.currency || 'RSD', + }, + sku: firstVariant?.sku, + availability: firstVariant?.quantityAvailable && firstVariant.quantityAvailable > 0 ? 'InStock' as const : 'OutOfStock' as const, + }; + return ( <> +
; } -export async function generateMetadata({ params }: ProductsPageProps) { +export async function generateMetadata({ params }: ProductsPageProps): Promise { const { locale } = await params; const validLocale = isValidLocale(locale) ? locale : DEFAULT_LOCALE; const metadata = getPageMetadata(validLocale as Locale); + const keywords = getPageKeywords(validLocale as Locale, 'products'); + + // Build canonical URL + const localePrefix = validLocale === DEFAULT_LOCALE ? "" : `/${validLocale}`; + const canonicalUrl = `${baseUrl}${localePrefix}/products`; + return { title: metadata.products.title, description: metadata.products.description, + keywords: [...keywords.primary, ...keywords.secondary].join(', '), + alternates: { + canonical: canonicalUrl, + }, + openGraph: { + title: metadata.products.title, + description: metadata.products.description, + type: 'website', + url: canonicalUrl, + images: [{ + url: `${baseUrl}/og-image.jpg`, + width: 1200, + height: 630, + alt: metadata.products.title, + }], + locale: validLocale, + }, }; } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index e8ffdcd..168d9d0 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,6 +2,7 @@ import "./globals.css"; import type { Metadata, Viewport } from "next"; import ErrorBoundary from "@/components/providers/ErrorBoundary"; import { SUPPORTED_LOCALES } from "@/lib/i18n/locales"; +import { OrganizationSchema } from "@/components/seo"; const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://dev.manoonoils.com"; @@ -43,6 +44,12 @@ export default async function RootLayout({ {children} + );