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 || (process.env.NODE_ENV === 'production' ? 'https://api.canina.ir/api' : 'http://127.0.0.1:4400/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 */ /** * Clean and strip any repeated brand affixes from a title. * Removes occurrences of brand names (Fa / En) and separators so the template or title can format cleanly. */ export function cleanPageTitle(rawTitle: string, brandName = 'کنینا ایران'): string { if (!rawTitle || !rawTitle.trim()) { return ''; } let clean = rawTitle.trim(); // Known brand aliases to strip to avoid repetitive keyword stuffing const brandKeywords = [ brandName, 'کنینا ایران', 'فروشگاه کنینا ایران', 'فروشگاه کنینا', 'کنینا', 'Canina Iran', 'Canina', ]; for (const b of brandKeywords) { if (!b) continue; const escaped = b.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const regex = new RegExp(`(\\s*[-|•/]\\s*${escaped}|${escaped}\\s*[-|•/]\\s*)`, 'gi'); clean = clean.replace(regex, ' ').trim(); } // Remove dangling separators at beginning or end clean = clean.replace(/^[-|•/\s]+|[-|•/\s]+$/g, '').trim(); return clean; } /** * Uniform Page Title Formatter according to Store Brand Configuration * For Next.js subpages, returns a clean title so layout.tsx template can affix brand name cleanly without repetition. */ 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, return the full configured home title as-is if (isHomePage) { return clean; } // Clean any brand suffixes/prefixes already embedded in rawTitle const baseTitle = cleanPageTitle(clean, brandName); // Return baseTitle directly so layout.tsx's `title.template` handles affixing brand name exactly once! // If baseTitle becomes empty (e.g. rawTitle was just "کنینا ایران"), fallback to rawTitle or brandName return baseTitle || clean || 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'))); const ogTitle = isHome ? title : (titlePosition === 'prefix' ? `${brandNameFa} ${titleSeparator} ${title}` : `${title} ${titleSeparator} ${brandNameFa}`); return { title: isHome ? { absolute: title } : title, description, keywords, alternates: { canonical: canonicalUrl, }, openGraph: { title: ogTitle, description, url: canonicalUrl, siteName: brandNameFa, locale: 'fa_IR', type: 'website', images: [ { url: image, width: 1200, height: 630, alt: ogTitle, }, ], }, twitter: { card: 'summary_large_image', title: ogTitle, 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, }, }, }; }