მეტადატა (title, description, keywords)
პროდუქტი, კატეგორია და ბლოგპოსტი თითო-თითო LocalizedText ველად ინახავენ metaTitle-ს, metaDescription-სა და metaKeywords-ს. მნიშვნელობები რენდერის დროს რესოლვდება i18nPageMetadataAsync-ის მეშვეობით lib/seo.ts-ში და აბრუნდება Next.js-ის generateMetadata-ს. თუ მოვაჭრეს ველი ცარიელი აქვს, KOMERO იყენებს გონივრულ default-ს (პროდუქტის სახელი / კატეგორიის სახელი / ბლოგის სათაური). ცარიელი description-ი შედის entity description-ის პირველ 160 სიმბოლოზე entity references-ის გამოკლების შემდეგ.
// types/product.ts
export type ProductModel = {
metaTitle?: LocalizedText;
metaDescription?: LocalizedText;
metaKeywords?: LocalizedText;
canonicalOverride?: string;
slug: string;
isIndexable: boolean;
// ...
};
Canonical URL-ის რესოლუცია
Canonical URL რესოლვდება ამ თანმიმდევრობით: product.canonicalOverride (ვალიდირებული URL) → siteConfig.seo.canonicalBaseUrl + path + locale → siteConfig.url + path + locale. კატეგორია და დინამიური გვერდები ჯერ მხოლოდ tenant-wide base override-ს უჭერენ მხარს; per-page override roadmap-ზეა.
// lib/seo.ts
function getBASE(seo: SEOConfig, siteUrl: string) {
return seo.canonicalBaseUrl?.trim() || siteUrl;
}
JSON-LD სქემები
მაღაზია გამოსცემს Product, Organization (ContactPoint / PostalAddress / GeoCoordinates / OpeningHoursSpecification-ით), BreadcrumbList, Website (SearchAction-ით), BlogPosting, ItemList და FAQPage. AggregateRating სტრუქტურა არსებობს buildProductJsonLd-ში, მაგრამ არ ავსდება სანამ ratings backend არ ჩაირთვება.
// lib/seo.ts
export function buildProductJsonLd(product, siteConfig, locale) {
return {
'@context': 'https://schema.org',
'@type': 'Product',
name, description, image, brand,
offers: { '@type': 'Offer', price, priceCurrency, availability, url },
// aggregateRating?: { ratingValue, reviewCount } // ← ratings roadmap
};
}
Sitemap.xml
გენერირდება app/sitemap.ts-ში force-dynamic + revalidate = 3600 კონფიგურაციით. Per-host და per-tenant. მოიცავს პროდუქტებს (მხოლოდ როცა isIndexable არის true), კატეგორიებს, ბრენდებს, ბლოგპოსტებს, ბლოგის კატეგორიებს, ბლოგის ტეგებს, დინამიურ გვერდებს და info გვერდებს. პროდუქტის ჩანაწერი მოიცავს <image:image> child-ს კავერის URL-ით. არავალიდური ან 2000-მდე თარიღები უარიყოფა.
// app/sitemap.ts
export const dynamic = 'force-dynamic';
export const revalidate = 3600;
export default async function sitemap() {
// fetch tenant, products, categories, blog...
return entries;
}
Robots.txt (AI კრაულერების პოლიტიკა)
app/robots.ts არის force-dynamic და per-host. საწარმოო ჰოსტები აშვებენ GPTBot-ს, OAI-SearchBot-ს, ChatGPT-User-ს, ClaudeBot-სა და PerplexityBot-ს AI ძიების ხილვადობისთვის. ტრეინინგისთვის განკუთვნილი კრაულერები (CCBot, anthropic-ai, Bytespider) დახურულია. Staging ჰოსტები აბრუნებენ Disallow: / ყოველი user agent-ისთვის draft მაღაზიების დასაცავად.
// app/robots.ts (production)
User-agent: GPTBot
Allow: /
User-agent: ClaudeBot
Allow: /
User-agent: CCBot
Disallow: /
Hreflang & x-default
buildI18nUrls აბრუნებს { canonical, languages } path-ისთვის ყოველ აქტიურ tenant ლოკალზე. x-default უჩვენებს tenant-ის fallback ლოკალს (ჩვეულებრივ ka ქართული მოვაჭრეებისთვის, en საერთაშორისოსთვის). Next.js გამოსცემს, როგორც <link rel='alternate' hreflang='...' href='...'>.
// lib/seo.ts
const { canonical, languages } = buildI18nUrls('/product/xyz', activeLocales, siteConfig);
return { alternates: { canonical, languages } };
სურათის alt ტექსტი
ProductImageModel.altText არის LocalizedText, ასე რომ ყოველი აქტიური ლოკალი შეიძლება ჰქონდეს საკუთარი alt ტექსტი სურათზე. მართვა ხდება ადმინის ReviewImagesModal-ში, სადაც არის drag-to-reorder, cover-ის არჩევა, წაშლა + undo, paste upload და სურათის კომპრესია. Alt ტექსტი არასდროს გენერირდება ავტომატურად - მოვაჭრე მას ხელით უთითებს.
// types/product.ts
export type ProductImageModel = {
imagePath: string;
altText: LocalizedText;
isCover: boolean;
order: number;
};
ანალიტიკის სკრიპტის ინიექცია
GA4, GTM, Meta Pixel, Hotjar და Microsoft Clarity ID-ები ინახება tenant SEOConfig-ში და ინიექცირდება AnalyticsScripts.tsx-ის მეშვეობით. ყოველი სკრიპტი consent gate-შია გახვეული, რომელიც ითვალისწინებს cookie consent კატეგორიებს (analytics vs marketing). თავისუფალი <script> ინიექცია განზრახ არ არის დაშვებული უსაფრთხოებისა და პროდუქტიულობის მიზნით.
// components/marketing/analytics/AnalyticsScripts.tsx
if (consent.analytics && seo.googleAnalyticsId) {
loadGA4(seo.googleAnalyticsId);
}
AI-გენერირებული მეტადატა (Gemini)
/api/ai/generate-seo endpoint-ი მიმართავს Google Gemini-ს @ai-sdk/google Vercel AI SDK-ის მეშვეობით და გენერირებს metaTitle-ს, metaDescription-სა და metaKeywords-ს პროდუქტისთვის, კატეგორიისთვის ან ბლოგპოსტისთვის entity name-ის, description-ისა და ლოკალის საფუძველზე. Draft-ები ჩერდება პირდაპირ ადმინის ფორმაში, სადაც მოვაჭრე მას გადახედავს და შეინახავს.
// api/ai/generate-seo
POST { entityType, entityId, locale }
=> { metaTitle, metaDescription, metaKeywords }