- Update [slug]/page.tsx to always include locale in canonical URL
- Update by-oil/page.tsx to use consistent locale prefix
- Update by-concern/page.tsx to use consistent locale prefix
- All canonical URLs now match actual URLs: /{locale}/solutions/{slug}
214 lines
7.3 KiB
TypeScript
214 lines
7.3 KiB
TypeScript
import { Metadata } from "next";
|
|
import Link from "next/link";
|
|
import { getTranslations } from "next-intl/server";
|
|
import { ChevronRight, Droplets } from "lucide-react";
|
|
import Header from "@/components/layout/Header";
|
|
import Footer from "@/components/layout/Footer";
|
|
import { getAllOilForConcernPages, getLocalizedString, getAllOils } from "@/lib/programmatic-seo/dataLoader";
|
|
import { isValidLocale, DEFAULT_LOCALE } from "@/lib/i18n/locales";
|
|
|
|
type Params = Promise<{ locale: string }>;
|
|
|
|
export async function generateMetadata({
|
|
params,
|
|
}: {
|
|
params: Params;
|
|
}): Promise<Metadata> {
|
|
const { locale } = await params;
|
|
const validLocale = isValidLocale(locale) ? locale : DEFAULT_LOCALE;
|
|
const t = await getTranslations({ locale: validLocale, namespace: "Solutions.ByOil" });
|
|
|
|
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://manoonoils.com";
|
|
const localePrefix = `/${validLocale}`;
|
|
const canonicalUrl = `${baseUrl}${localePrefix}/solutions/by-oil`;
|
|
|
|
return {
|
|
title: t("metaTitle"),
|
|
description: t("metaDescription"),
|
|
alternates: {
|
|
canonical: canonicalUrl,
|
|
},
|
|
openGraph: {
|
|
url: canonicalUrl,
|
|
},
|
|
};
|
|
}
|
|
|
|
interface OilCardProps {
|
|
oilName: string;
|
|
concernCount: number;
|
|
concerns: string[];
|
|
locale: string;
|
|
firstSolutionSlug: string;
|
|
bestFor: string;
|
|
exploreSolutions: string;
|
|
solutionLabel: string;
|
|
solutionsLabel: string;
|
|
shortDescription: string;
|
|
}
|
|
|
|
function OilCard({
|
|
oilName,
|
|
concernCount,
|
|
concerns,
|
|
locale,
|
|
firstSolutionSlug,
|
|
bestFor,
|
|
exploreSolutions,
|
|
solutionLabel,
|
|
solutionsLabel,
|
|
shortDescription,
|
|
}: OilCardProps) {
|
|
return (
|
|
<div className="border border-[#e5e5e5] rounded-lg p-6 hover:border-black transition-colors group flex flex-col">
|
|
<div className="flex items-center gap-3 mb-4">
|
|
<div className="w-10 h-10 rounded-full bg-amber-100 flex items-center justify-center">
|
|
<Droplets className="w-5 h-5 text-amber-700" />
|
|
</div>
|
|
<h3 className="text-lg font-medium text-[#1a1a1a]">
|
|
{oilName}
|
|
</h3>
|
|
</div>
|
|
|
|
<p className="text-sm text-[#666666] mb-4 flex-grow">
|
|
{shortDescription}
|
|
</p>
|
|
|
|
<p className="text-sm text-[#666666] mb-4">
|
|
{concernCount} {concernCount === 1 ? solutionLabel : solutionsLabel}
|
|
</p>
|
|
|
|
<div className="space-y-2 mb-4">
|
|
<p className="text-xs uppercase tracking-wider text-[#999999] font-medium">
|
|
{bestFor}
|
|
</p>
|
|
{concerns.slice(0, 3).map((concernName) => (
|
|
<div key={concernName} className="flex items-center gap-2 text-sm text-[#666666]">
|
|
<div className="w-1.5 h-1.5 rounded-full bg-emerald-400" />
|
|
{concernName}
|
|
</div>
|
|
))}
|
|
{concerns.length > 3 && (
|
|
<p className="text-xs text-[#999999]">
|
|
+{concerns.length - 3} more
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<Link
|
|
href={`/${locale}/solutions/${firstSolutionSlug}`}
|
|
className="inline-flex items-center text-sm font-medium text-[#1a1a1a] group-hover:text-black transition-colors mt-auto"
|
|
>
|
|
{exploreSolutions}
|
|
<ChevronRight className="ml-1 w-4 h-4 transform group-hover:translate-x-1 transition-transform" />
|
|
</Link>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default async function ByOilPage({
|
|
params,
|
|
}: {
|
|
params: Params;
|
|
}) {
|
|
const { locale } = await params;
|
|
const t = await getTranslations({ locale, namespace: "Solutions" });
|
|
const pageT = await getTranslations({ locale, namespace: "Solutions.ByOil" });
|
|
|
|
const [oils, allPages] = await Promise.all([
|
|
getAllOils(),
|
|
getAllOilForConcernPages(),
|
|
]);
|
|
|
|
const oilsWithPages = oils
|
|
.map(oil => {
|
|
const pages = allPages.filter(page => page.oilId === oil.id);
|
|
return {
|
|
...oil,
|
|
pages,
|
|
concernCount: pages.length,
|
|
};
|
|
})
|
|
.filter(oil => oil.pages.length > 0)
|
|
.sort((a, b) => a.name[locale as "sr" | "en" | "de" | "fr"].localeCompare(b.name[locale as "sr" | "en" | "de" | "fr"]));
|
|
|
|
return (
|
|
<>
|
|
<Header locale={locale} hideLangSwitcher={true} />
|
|
<div className="min-h-screen bg-white">
|
|
<section className="pt-32 pb-16 lg:pt-40 lg:pb-24">
|
|
<div className="container">
|
|
<nav className="flex items-center gap-2 text-sm text-[#666666] mb-8">
|
|
<Link href={`/${locale}`} className="hover:text-black transition-colors">
|
|
{t("breadcrumb.home")}
|
|
</Link>
|
|
<ChevronRight className="w-4 h-4" />
|
|
<Link href={`/${locale}/solutions`} className="hover:text-black transition-colors">
|
|
{t("breadcrumb.solutions")}
|
|
</Link>
|
|
<ChevronRight className="w-4 h-4" />
|
|
<span className="text-[#1a1a1a]">{t("breadcrumb.byOil")}</span>
|
|
</nav>
|
|
|
|
<div className="max-w-3xl mb-12">
|
|
<h1 className="text-4xl lg:text-5xl font-medium tracking-tight text-[#1a1a1a] mb-6">
|
|
{pageT("title")}
|
|
</h1>
|
|
<p className="text-lg text-[#666666] leading-relaxed">
|
|
{pageT("subtitle")}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="bg-gradient-to-r from-amber-50 to-emerald-50 border border-[#e5e5e5] rounded-lg p-6 mb-12">
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-12 h-12 rounded-full bg-white flex items-center justify-center shadow-sm">
|
|
<Droplets className="w-6 h-6 text-amber-600" />
|
|
</div>
|
|
<div>
|
|
<p className="text-sm text-[#666666]">
|
|
{pageT("stats.availableOils", { count: oilsWithPages.length })}
|
|
</p>
|
|
<p className="text-sm text-[#666666]">
|
|
{pageT("stats.totalSolutions", { count: allPages.length })}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{oilsWithPages.map((oil) => {
|
|
const oilName = getLocalizedString(oil.name, locale);
|
|
const shortDescription = getLocalizedString(oil.shortDescription, locale);
|
|
const concerns = oil.pages.map(p => getLocalizedString(p.concernName, locale));
|
|
|
|
return (
|
|
<OilCard
|
|
key={oil.id}
|
|
oilName={oilName}
|
|
concernCount={oil.concernCount}
|
|
concerns={concerns}
|
|
locale={locale}
|
|
firstSolutionSlug={oil.pages[0].localizedSlugs[locale as "sr" | "en" | "de" | "fr"]}
|
|
bestFor={pageT("oilCard.bestFor")}
|
|
exploreSolutions={pageT("oilCard.exploreSolutions")}
|
|
solutionLabel={pageT("oilCard.solutionLabel")}
|
|
solutionsLabel={pageT("oilCard.solutionsLabel")}
|
|
shortDescription={shortDescription}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{oilsWithPages.length === 0 && (
|
|
<div className="text-center py-16">
|
|
<p className="text-[#666666]">{pageT("noResults")}</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</section>
|
|
</div>
|
|
<Footer locale={locale} />
|
|
</>
|
|
);
|
|
}
|