import type { Metadata } from 'next'; export interface SeoConfig { brandNameFa: string; brandNameEn: string; titleSeparator: string; titlePosition: 'suffix' | 'prefix' | 'none'; defaultMetaTitle: string; defaultMetaDescription: string; keywords: string[]; canonicalBaseUrl: string; ogImageUrl: string; googleSiteVerification?: string; googleAnalyticsId?: string; googleTagManagerId?: string; enableGoogleAnalytics?: boolean; enableGoogleTagManager?: boolean; clarityProjectId?: string; texts: Record; } const DEFAULT_SEO_CONFIG: SeoConfig = { brandNameFa: 'کنینا ایران', brandNameEn: 'Canina Iran', titleSeparator: '|', titlePosition: 'suffix', defaultMetaTitle: 'کنینا ایران | نماینده رسمی مکمل‌های دارویی Canina آلمان', defaultMetaDescription: 'فروشگاه تخصصی و نماینده انحصاری مکمل‌های درمانی و تقویتی سگ و گربه کنینا آلمان با فرمولاسیون دارویی و تاییدیه بالینی.', keywords: ['کنینا', 'مکمل حیوانات خانگی', 'مکمل سگ', 'مکمل گربه', 'داروی حیوانات', 'Canina Iran'], canonicalBaseUrl: 'https://canina.ir', ogImageUrl: '/assets/images/regenerated_image_1779109861747.png', googleSiteVerification: '', googleAnalyticsId: '', googleTagManagerId: '', enableGoogleAnalytics: true, enableGoogleTagManager: true, clarityProjectId: '', texts: {}, }; /** * Fetch dynamic SEO settings and UI texts on server with 60-second revalidation */ export async function getSeoConfig(): Promise { const apiBase = process.env.INTERNAL_API_URL || process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4001/api'; try { const res = await fetch(`${apiBase}/settings/ui-texts`, { next: { revalidate: 60, tags: ['ui-texts', 'settings'] }, }); if (res.ok) { const data = await res.json(); const texts: Record = {}; if (Array.isArray(data)) { data.forEach((item: { key: string; value: string }) => { if (item?.key) texts[item.key] = item.value; }); } else if (typeof data === 'object' && data !== null) { Object.entries(data).forEach(([k, v]) => { texts[k] = String(v); }); } const brandNameFa = texts['brand_name_fa'] || texts['BRAND_LOGO_TEXT_FA'] || texts['site_logo_text_fa'] || DEFAULT_SEO_CONFIG.brandNameFa; const brandNameEn = texts['brand_name_en'] || texts['BRAND_LOGO_TEXT_EN'] || DEFAULT_SEO_CONFIG.brandNameEn; const titleSeparator = texts['title_separator'] || DEFAULT_SEO_CONFIG.titleSeparator; const titlePositionRaw = texts['title_position']?.toLowerCase(); const titlePosition: 'suffix' | 'prefix' | 'none' = titlePositionRaw === 'prefix' || titlePositionRaw === 'none' ? titlePositionRaw : 'suffix'; const defaultMetaTitle = texts['defaultMetaTitle'] || texts['home_meta_title'] || DEFAULT_SEO_CONFIG.defaultMetaTitle; const defaultMetaDescription = texts['defaultMetaDescription'] || texts['home_meta_desc'] || DEFAULT_SEO_CONFIG.defaultMetaDescription; const rawKeywords = texts['keywords'] || texts['home_keywords']; const keywords = rawKeywords ? rawKeywords.split(',').map((k) => k.trim()).filter(Boolean) : DEFAULT_SEO_CONFIG.keywords; const canonicalBaseUrl = texts['canonicalBaseUrl'] || process.env.NEXT_PUBLIC_SITE_URL || DEFAULT_SEO_CONFIG.canonicalBaseUrl; const ogImageUrl = texts['ogImageUrl'] || DEFAULT_SEO_CONFIG.ogImageUrl; const googleSiteVerification = texts['googleSiteVerification'] || texts['google_site_verification'] || process.env.NEXT_PUBLIC_GOOGLE_SITE_VERIFICATION || ''; const googleAnalyticsId = texts['googleAnalyticsId'] || texts['ga_measurement_id'] || process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID || ''; const googleTagManagerId = texts['googleTagManagerId'] || texts['gtm_id'] || process.env.NEXT_PUBLIC_GTM_ID || ''; const enableGoogleAnalytics = texts['enableGoogleAnalytics'] !== undefined ? texts['enableGoogleAnalytics'] === 'true' : true; const enableGoogleTagManager = texts['enableGoogleTagManager'] !== undefined ? texts['enableGoogleTagManager'] === 'true' : true; const clarityProjectId = texts['clarityProjectId'] || texts['clarity_project_id'] || process.env.NEXT_PUBLIC_CLARITY_PROJECT_ID || ''; return { brandNameFa, brandNameEn, titleSeparator, titlePosition, defaultMetaTitle, defaultMetaDescription, keywords, canonicalBaseUrl, ogImageUrl, googleSiteVerification, googleAnalyticsId, googleTagManagerId, enableGoogleAnalytics, enableGoogleTagManager, clarityProjectId, texts, }; } } catch (error) { console.warn('[SEO] Unable to fetch dynamic settings from backend, using fallback:', error); } return DEFAULT_SEO_CONFIG; } /** * Uniform Page Title Formatter according to Store Brand Configuration */ export function formatPageTitle( rawTitle: string, brandName = 'کنینا ایران', separator = '|', position: 'suffix' | 'prefix' | 'none' = 'suffix', isHomePage = false ): string { if (!rawTitle || !rawTitle.trim()) { return brandName; } const clean = rawTitle.trim(); // If this is the home page and has a custom full title configured, return it directly if (isHomePage) { return clean; } // Check if the title already explicitly contains the brand name const escapedBrand = brandName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const brandRegex = new RegExp(`(\\s*[-|•]\\s*${escapedBrand}|${escapedBrand}\\s*[-|•]\\s*)`, 'gi'); // Strip existing repeated brand affix if present so we can format cleanly const baseTitle = clean.replace(brandRegex, '').trim(); if (position === 'none') { return baseTitle || clean; } if (position === 'prefix') { return `${brandName} ${separator} ${baseTitle || clean}`; } // Default: 'suffix' -> "{PageTitle} {separator} {BrandName}" return `${baseTitle || clean} ${separator} ${brandName}`; } export interface PageMetadataOptions { fallbackTitle?: string; fallbackDesc?: string; fallbackKeywords?: string[]; canonicalPath?: string; ogImage?: string; isDynamicEntity?: boolean; // For PDP / Blog / Wiki detail noIndex?: boolean; } /** * Generate standard Next.js Metadata object dynamically from server settings */ export async function getPageMetadata( pageKey: string, options: PageMetadataOptions = {} ): Promise { const config = await getSeoConfig(); const { texts, brandNameFa, titleSeparator, titlePosition, canonicalBaseUrl, ogImageUrl } = config; const isHome = pageKey === 'home'; const rawTitle = texts[`${pageKey}_meta_title`] || options.fallbackTitle || config.defaultMetaTitle; const title = options.isDynamicEntity ? formatPageTitle(rawTitle, brandNameFa, titleSeparator, titlePosition, false) : formatPageTitle(rawTitle, brandNameFa, titleSeparator, titlePosition, isHome); const description = texts[`${pageKey}_meta_desc`] || options.fallbackDesc || config.defaultMetaDescription; const rawKeywords = texts[`${pageKey}_keywords`]; const keywords = rawKeywords ? rawKeywords.split(',').map((k) => k.trim()).filter(Boolean) : options.fallbackKeywords || config.keywords; const explicitCanonical = texts[`${pageKey}_canonical`]; const relativePath = options.canonicalPath ?? (isHome ? '/' : `/${pageKey.replace(/_/g, '-')}`); const canonicalUrl = explicitCanonical || `${canonicalBaseUrl.replace(/\/$/, '')}${relativePath}`; const image = options.ogImage || ogImageUrl; const isStaging = options.noIndex || process.env.NEXT_PUBLIC_APP_ENV === 'staging' || process.env.NODE_ENV !== 'production' || (typeof process.env.NEXT_PUBLIC_SITE_URL === 'string' && (process.env.NEXT_PUBLIC_SITE_URL.includes('stage') || process.env.NEXT_PUBLIC_SITE_URL.includes('test') || process.env.NEXT_PUBLIC_SITE_URL.includes('dev'))); return { title, description, keywords, alternates: { canonical: canonicalUrl, }, openGraph: { title, description, url: canonicalUrl, siteName: brandNameFa, locale: 'fa_IR', type: 'website', images: [ { url: image, width: 1200, height: 630, alt: title, }, ], }, twitter: { card: 'summary_large_image', title, description, images: [image], }, robots: isStaging ? { index: false, follow: false, nocache: true, googleBot: { index: false, follow: false, noimageindex: true, }, } : { index: true, follow: true, googleBot: { index: true, follow: true, 'max-video-preview': -1, 'max-image-preview': 'large', 'max-snippet': -1, }, }, }; }