feat: implement programmatic SEO for solutions pages

- Add 10 oil-for-concern solution pages with localized slugs
- Support 4 languages: sr, en, de, fr with proper canonical URLs
- Add solutions hub, by-concern, and by-oil directory pages
- Filter bundle products from solutions pages
- Add hideLangSwitcher prop to Header component
- Update translations for all languages
- Fix canonical URLs to include locale prefix
This commit is contained in:
Unchained
2026-04-08 13:29:42 +02:00
parent cca6f44139
commit 9d07a60d3f
26 changed files with 3120 additions and 1046 deletions
+37 -34
View File
@@ -14,9 +14,10 @@ import type { Locale } from "@/lib/i18n/locales";
interface HeaderProps {
locale?: string;
hideLangSwitcher?: boolean;
}
export default function Header({ locale: propLocale = "sr" }: HeaderProps) {
export default function Header({ locale: propLocale = "sr", hideLangSwitcher = false }: HeaderProps) {
const t = useTranslations("Header");
const pathname = usePathname();
const dropdownRef = useRef<HTMLDivElement>(null);
@@ -132,39 +133,41 @@ export default function Header({ locale: propLocale = "sr" }: HeaderProps) {
</Link>
<div className="flex items-center gap-1">
<div ref={dropdownRef} className="relative">
<button
className="p-2 hover:bg-black/5 rounded-full transition-colors flex items-center gap-1"
onClick={() => setLangDropdownOpen(!langDropdownOpen)}
aria-label="Select language"
>
<Globe className="w-5 h-5" strokeWidth={1.5} />
<span className="text-sm">{currentLocale.flag}</span>
</button>
<AnimatePresence>
{langDropdownOpen && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="absolute right-0 top-full mt-1 bg-white border border-[#e5e5e5] shadow-lg rounded-md overflow-hidden z-50"
>
{SUPPORTED_LOCALES.map((loc) => (
<button
key={loc}
onClick={() => switchLocale(loc)}
className={`flex items-center gap-2 px-4 py-2 text-sm hover:bg-black/5 transition-colors w-full text-left ${
loc === locale ? "bg-black/5 font-medium" : ""
}`}
>
<span>{LOCALE_CONFIG[loc].flag}</span>
<span>{LOCALE_CONFIG[loc].label}</span>
</button>
))}
</motion.div>
)}
</AnimatePresence>
</div>
{!hideLangSwitcher && (
<div ref={dropdownRef} className="relative">
<button
className="p-2 hover:bg-black/5 rounded-full transition-colors flex items-center gap-1"
onClick={() => setLangDropdownOpen(!langDropdownOpen)}
aria-label="Select language"
>
<Globe className="w-5 h-5" strokeWidth={1.5} />
<span className="text-sm">{currentLocale.flag}</span>
</button>
<AnimatePresence>
{langDropdownOpen && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="absolute right-0 top-full mt-1 bg-white border border-[#e5e5e5] shadow-lg rounded-md overflow-hidden z-50"
>
{SUPPORTED_LOCALES.map((loc) => (
<button
key={loc}
onClick={() => switchLocale(loc)}
className={`flex items-center gap-2 px-4 py-2 text-sm hover:bg-black/5 transition-colors w-full text-left ${
loc === locale ? "bg-black/5 font-medium" : ""
}`}
>
<span>{LOCALE_CONFIG[loc].flag}</span>
<span>{LOCALE_CONFIG[loc].label}</span>
</button>
))}
</motion.div>
)}
</AnimatePresence>
</div>
)}
<button
className="p-2 hover:bg-black/5 rounded-full transition-colors hidden sm:block"
@@ -30,7 +30,7 @@ export function OilForConcernPageTemplate({ page, locale, basePath, products }:
return (
<>
<Header locale={locale} />
<Header locale={locale} hideLangSwitcher={true} />
<main className="min-h-screen bg-white">
<section className="bg-[#FAF9F7] pt-[180px] lg:pt-[200px] pb-16">
<div className="max-w-[1400px] mx-auto px-4 sm:px-6 lg:px-8">