feat: add hreflang tags and international sitemap for SEO

- Added hreflang alternates to root layout for all locales (sr, en, de, fr)
- Added hreflang alternates to [locale] layout for all locales
- Updated sitemap to include all locale variants for every page
- Google will now properly index all language versions
This commit is contained in:
Unchained
2026-03-24 11:22:22 +02:00
parent 52b2eac5b5
commit a4e7a07adb
3 changed files with 123 additions and 14 deletions

View File

@@ -1,11 +1,36 @@
import { Metadata } from "next";
import { NextIntlClientProvider } from "next-intl";
import { getMessages, setRequestLocale } from "next-intl/server";
import { routing } from "@/i18n/routing";
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://dev.manoonoils.com";
export function generateStaticParams() {
return routing.locales.map((locale) => ({ locale }));
}
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}): Promise<Metadata> {
const { locale } = await params;
const localePrefix = locale === "sr" ? "" : `/${locale}`;
const languages: Record<string, string> = {};
for (const loc of routing.locales) {
const prefix = loc === "sr" ? "" : `/${loc}`;
languages[loc] = `${baseUrl}${prefix}`;
}
return {
alternates: {
canonical: `${baseUrl}${localePrefix}`,
languages,
},
};
}
export default async function LocaleLayout({
children,
params,
@@ -22,4 +47,4 @@ export default async function LocaleLayout({
{children}
</NextIntlClientProvider>
);
}
}

View File

@@ -2,6 +2,8 @@ import "./globals.css";
import type { Metadata, Viewport } from "next";
import ErrorBoundary from "@/components/providers/ErrorBoundary";
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://dev.manoonoils.com";
export const metadata: Metadata = {
title: {
default: "ManoonOils - Premium Natural Oils for Hair & Skin",
@@ -9,6 +11,15 @@ export const metadata: Metadata = {
},
description: "Discover our premium collection of natural oils for hair and skin care.",
robots: "index, follow",
alternates: {
canonical: baseUrl,
languages: {
sr: baseUrl,
en: `${baseUrl}/en`,
de: `${baseUrl}/de`,
fr: `${baseUrl}/fr`,
},
},
openGraph: {
title: "ManoonOils - Premium Natural Oils for Hair & Skin",
description: "Discover our premium collection of natural oils for hair and skin care.",

View File

@@ -1,48 +1,121 @@
import { MetadataRoute } from "next";
import { getProducts } from "@/lib/saleor";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://dev.manoonoils.com";
const LOCALES = ["sr", "en", "de", "fr"] as const;
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://dev.manoonoils.com";
interface SitemapEntry {
url: string;
lastModified: Date;
changeFrequency: "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never";
priority: number;
alternates?: {
languages?: Record<string, string>;
};
}
export default async function sitemap(): Promise<SitemapEntry[]> {
let products: any[] = [];
try {
products = await getProducts("SR", 100);
} catch (e) {
console.log('Failed to fetch products for sitemap during build');
console.log("Failed to fetch products for sitemap during build");
}
const productUrls = products.map((product) => ({
url: `${baseUrl}/products/${product.slug}`,
lastModified: new Date(),
changeFrequency: "weekly" as const,
priority: 0.8,
}));
return [
const staticPages: SitemapEntry[] = [
{
url: baseUrl,
lastModified: new Date(),
changeFrequency: "daily",
priority: 1,
alternates: {
languages: {
sr: `${baseUrl}`,
en: `${baseUrl}/en`,
de: `${baseUrl}/de`,
fr: `${baseUrl}/fr`,
},
},
},
{
url: `${baseUrl}/products`,
lastModified: new Date(),
changeFrequency: "daily",
priority: 0.9,
alternates: {
languages: {
sr: `${baseUrl}/products`,
en: `${baseUrl}/en/products`,
de: `${baseUrl}/de/products`,
fr: `${baseUrl}/fr/products`,
},
},
},
{
url: `${baseUrl}/about`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.6,
alternates: {
languages: {
sr: `${baseUrl}/about`,
en: `${baseUrl}/en/about`,
de: `${baseUrl}/de/about`,
fr: `${baseUrl}/fr/about`,
},
},
},
{
url: `${baseUrl}/contact`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.6,
alternates: {
languages: {
sr: `${baseUrl}/contact`,
en: `${baseUrl}/en/contact`,
de: `${baseUrl}/de/contact`,
fr: `${baseUrl}/fr/contact`,
},
},
},
{
url: `${baseUrl}/checkout`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.5,
alternates: {
languages: {
sr: `${baseUrl}/checkout`,
en: `${baseUrl}/en/checkout`,
de: `${baseUrl}/de/checkout`,
fr: `${baseUrl}/fr/checkout`,
},
},
},
...productUrls,
];
const productUrls: SitemapEntry[] = [];
for (const product of products) {
for (const locale of LOCALES) {
const localePrefix = locale === "sr" ? "" : `/${locale}`;
productUrls.push({
url: `${baseUrl}${localePrefix}/products/${product.slug}`,
lastModified: new Date(),
changeFrequency: "weekly",
priority: 0.8,
alternates: {
languages: {
sr: `${baseUrl}/products/${product.slug}`,
en: `${baseUrl}/en/products/${product.slug}`,
de: `${baseUrl}/de/products/${product.slug}`,
fr: `${baseUrl}/fr/products/${product.slug}`,
},
},
});
}
}
return [...staticPages, ...productUrls];
}