Метаданные (заголовок, описание, ключевые слова)
Сущности товара, категории и поста блога предоставляют metaTitle, metaDescription и metaKeywords как поля LocalizedText. Значения разрешаются во время запроса через i18nPageMetadataAsync в lib/seo.ts и возвращаются через Next.js generateMetadata. Когда тенант оставляет поле пустым, KOMERO использует разумное значение по умолчанию (имя товара / имя категории / заголовок блога). Пустые описания заменяются первыми 160 символами описания сущности с удалением ссылок на сущности.
// types/product.ts
export type ProductModel = {
metaTitle?: LocalizedText;
metaDescription?: LocalizedText;
metaKeywords?: LocalizedText;
canonicalOverride?: string;
slug: string;
isIndexable: boolean;
// ...
};
Разрешение канонического URL
Канонические URL разрешаются в этом порядке: product.canonicalOverride (валидированный URL) → siteConfig.seo.canonicalBaseUrl + path + locale → siteConfig.url + path + locale. Категории и динамические страницы сейчас поддерживают только базовое переопределение для тенанта; переопределение для страницы в планах.
// 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, но не заполняется, пока не выйдет бэкенд рейтингов.
// 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 } // ← планы по рейтингам
};
}
Sitemap.xml
Генерируется в app/sitemap.ts с force-dynamic + revalidate = 3600. Для каждого хоста и тенанта. Включает товары (только когда isIndexable равно true), категории, бренды, посты блога, категории блога, теги блога, динамические страницы и статические информационные страницы. Записи товаров включают дочерний <image:image> с 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 и работает для каждого хоста. Продакшн-хосты разрешают GPTBot, OAI-SearchBot, ChatGPT-User, ClaudeBot и PerplexityBot для видимости в AI-поиске. Краулеры только для обучения (CCBot, anthropic-ai, Bytespider) заблокированы. Staging-хосты возвращают Disallow: / для каждого user agent, чтобы защитить черновые витрины от индексации.
// app/robots.ts (production)
User-agent: GPTBot
Allow: /
User-agent: ClaudeBot
Allow: /
User-agent: CCBot
Disallow: /
Hreflang и x-default
buildI18nUrls возвращает { canonical, languages } для пути по всем активным локалям тенанта. x-default устанавливается в резервную локаль тенанта (обычно 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, который поддерживает перетаскивание для изменения порядка, выбор обложки, удаление + отмену, загрузку вставкой и сжатие изображений. Alt-текст никогда не генерируется автоматически — тенанты предоставляют его вручную.
// types/product.ts
export type ProductImageModel = {
imagePath: string;
altText: LocalizedText;
isCover: boolean;
order: number;
};
Инжекция скриптов аналитики
ID для GA4, GTM, Meta Pixel, Hotjar и Microsoft Clarity хранятся в SEOConfig тенанта и инжектируются через AnalyticsScripts.tsx. Каждый скрипт обёрнут в гейт согласия, который уважает категории согласия на cookies (analytics vs marketing). Произвольная инжекция <script> умышленно не предоставляется по соображениям безопасности и производительности.
// components/marketing/analytics/AnalyticsScripts.tsx
if (consent.analytics && seo.googleAnalyticsId) {
loadGA4(seo.googleAnalyticsId);
}
AI-генерация метаданных (Gemini)
Эндпоинт /api/ai/generate-seo вызывает Google Gemini через Vercel AI SDK @ai-sdk/google для составления metaTitle, metaDescription и metaKeywords для товара, категории или поста блога на основе имени сущности, описания и локали. Черновики вставляются прямо в админскую форму, чтобы тенант мог просмотреть и сохранить.
// api/ai/generate-seo
POST { entityType, entityId, locale }
=> { metaTitle, metaDescription, metaKeywords }