diff --git a/.env.example b/.env.example index 5d6e98ce1..7411c1692 100644 --- a/.env.example +++ b/.env.example @@ -8,6 +8,20 @@ SPREE_PUBLISHABLE_KEY=your_publishable_api_key NEXT_PUBLIC_DEFAULT_COUNTRY=us NEXT_PUBLIC_DEFAULT_LOCALE=en NEXT_PUBLIC_STORE_NAME=Spree Store +NEXT_PUBLIC_STORE_DESCRIPTION=A modern e-commerce storefront powered by Spree Commerce and Next.js. + +# Store SEO settings (server-side only) +STORE_URL=https://your-store-domain.com +STORE_SEO_TITLE= +STORE_META_DESCRIPTION= +STORE_META_KEYWORDS= + +# Store social / Organization JSON-LD (optional) +STORE_TWITTER= +STORE_FACEBOOK= +STORE_INSTAGRAM= +STORE_LOGO_URL= +STORE_SUPPORT_EMAIL= # Google Tag Manager (leave empty to disable) GTM_ID= diff --git a/public/social-image.png b/public/social-image.png new file mode 100644 index 000000000..44c7e319b Binary files /dev/null and b/public/social-image.png differ diff --git a/src/app/[country]/[locale]/(checkout)/layout.tsx b/src/app/[country]/[locale]/(checkout)/layout.tsx index c0c1af9d8..5079cd4d7 100644 --- a/src/app/[country]/[locale]/(checkout)/layout.tsx +++ b/src/app/[country]/[locale]/(checkout)/layout.tsx @@ -19,7 +19,7 @@ function CheckoutHeader() { ; } +export async function generateMetadata({ + params, +}: CategoryPageProps): Promise { + const { country, locale, permalink } = await params; + return generateCategoryMetadata({ country, locale, permalink }); +} + export default async function CategoryPage({ params }: CategoryPageProps) { const { country, locale, permalink } = await params; const fullPermalink = permalink.join("/"); @@ -34,8 +45,14 @@ export default async function CategoryPage({ params }: CategoryPageProps) { notFound(); } + const storeUrl = getStoreUrl(); + return ( + {storeUrl && ( + + )} + {/* Banner Image — returns null when image missing or fails to load */} diff --git a/src/app/[country]/[locale]/(storefront)/page.tsx b/src/app/[country]/[locale]/(storefront)/page.tsx index 5513d9fbb..5de9a508e 100644 --- a/src/app/[country]/[locale]/(storefront)/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/page.tsx @@ -1,6 +1,9 @@ +import type { Metadata } from "next"; import Link from "next/link"; import { ProductCarousel } from "@/components/products/ProductCarousel"; import { Button } from "@/components/ui/button"; +import { generateHomeMetadata } from "@/lib/metadata/home"; +import { getStoreDescription, getStoreName } from "@/lib/seo"; interface HomePageProps { params: Promise<{ @@ -9,6 +12,13 @@ interface HomePageProps { }>; } +export async function generateMetadata({ + params, +}: HomePageProps): Promise { + const { country, locale } = await params; + return generateHomeMetadata({ country, locale }); +} + export default async function HomePage({ params }: HomePageProps) { const { country, locale } = await params; const basePath = `/${country}/${locale}`; @@ -20,11 +30,10 @@ export default async function HomePage({ params }: HomePageProps) { - Welcome to Spree Store + Welcome to {getStoreName()} - Discover amazing products with our modern e-commerce experience - powered by Spree Commerce. + {getStoreDescription()} diff --git a/src/app/[country]/[locale]/(storefront)/products/[slug]/page.tsx b/src/app/[country]/[locale]/(storefront)/products/[slug]/page.tsx index 9cf6b55d3..fd45fdb8c 100644 --- a/src/app/[country]/[locale]/(storefront)/products/[slug]/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/products/[slug]/page.tsx @@ -1,3 +1,8 @@ +import type { Metadata } from "next"; +import { JsonLd } from "@/components/seo/JsonLd"; +import { getCachedProduct } from "@/lib/data/cached"; +import { generateProductMetadata } from "@/lib/metadata/product"; +import { buildCanonicalUrl, buildProductJsonLd, getStoreUrl } from "@/lib/seo"; import { ProductDetailsWrapper } from "./ProductDetailsWrapper"; interface ProductPageProps { @@ -8,9 +13,39 @@ interface ProductPageProps { }>; } +export async function generateMetadata({ + params, +}: ProductPageProps): Promise { + const { country, locale, slug } = await params; + return generateProductMetadata({ country, locale, slug }); +} + export default async function ProductPage({ params }: ProductPageProps) { const { country, locale, slug } = await params; const basePath = `/${country}/${locale}`; - return ; + let product; + try { + product = await getCachedProduct(slug, ["images"], locale); + } catch { + product = null; + } + + const storeUrl = getStoreUrl(); + const canonicalUrl = + product && storeUrl + ? buildCanonicalUrl( + storeUrl, + `/${country}/${locale}/products/${product.slug}`, + ) + : undefined; + + return ( + <> + {product && canonicalUrl && ( + + )} + + > + ); } diff --git a/src/app/[country]/[locale]/(storefront)/products/page.tsx b/src/app/[country]/[locale]/(storefront)/products/page.tsx index 5595aa79b..3971640cd 100644 --- a/src/app/[country]/[locale]/(storefront)/products/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/products/page.tsx @@ -1,3 +1,5 @@ +import type { Metadata } from "next"; +import { generateProductsMetadata } from "@/lib/metadata/products"; import { ProductsContent } from "./ProductsContent"; interface ProductsPageProps { @@ -7,6 +9,13 @@ interface ProductsPageProps { }>; } +export async function generateMetadata({ + params, +}: ProductsPageProps): Promise { + const { country, locale } = await params; + return generateProductsMetadata({ country, locale }); +} + export default async function ProductsPage({ params }: ProductsPageProps) { const { country, locale } = await params; const basePath = `/${country}/${locale}`; diff --git a/src/app/[country]/[locale]/layout.tsx b/src/app/[country]/[locale]/layout.tsx index 5c337b0a8..2893f6426 100644 --- a/src/app/[country]/[locale]/layout.tsx +++ b/src/app/[country]/[locale]/layout.tsx @@ -1,6 +1,10 @@ +import type { Metadata } from "next"; +import { JsonLd } from "@/components/seo/JsonLd"; import { AuthProvider } from "@/contexts/AuthContext"; import { StoreProvider } from "@/contexts/StoreContext"; import { getMarkets } from "@/lib/data/markets"; +import { generateStoreMetadata } from "@/lib/metadata/store"; +import { buildOrganizationJsonLd } from "@/lib/seo"; interface CountryLocaleLayoutProps { children: React.ReactNode; @@ -10,6 +14,13 @@ interface CountryLocaleLayoutProps { }>; } +export async function generateMetadata({ + params, +}: CountryLocaleLayoutProps): Promise { + const { locale } = await params; + return generateStoreMetadata({ locale }); +} + export default async function CountryLocaleLayout({ children, params, @@ -26,7 +37,10 @@ export default async function CountryLocaleLayout({ initialLocale={locale} initialMarkets={markets} > - {children} + + + {children} + ); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 62d933318..746352b53 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -13,9 +13,16 @@ const geist = Geist({ subsets: ["latin"], }); +const rootStoreName = process.env.NEXT_PUBLIC_STORE_NAME || "Spree Store"; + export const metadata: Metadata = { - title: "Spree Storefront", - description: "Next.js storefront powered by Spree Commerce", + title: { + template: `%s | ${rootStoreName}`, + default: rootStoreName, + }, + description: + process.env.NEXT_PUBLIC_STORE_DESCRIPTION || + "A modern e-commerce storefront powered by Spree Commerce and Next.js.", }; export default function RootLayout({ diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx index f61a27fee..a5f4a64be 100644 --- a/src/components/layout/Footer.tsx +++ b/src/components/layout/Footer.tsx @@ -1,17 +1,18 @@ +"use client"; + import Link from "next/link"; +import { useStore } from "@/contexts/StoreContext"; export function Footer() { + const { storeName, storeDescription } = useStore(); return (
- Discover amazing products with our modern e-commerce experience - powered by Spree Commerce. + {getStoreDescription()}