Адреса страниц и переадресации
Адрес объекта: slug и slugs
У каждого объекта есть ЧПУ (человекопонятный адрес) на каждом языке. Его хранит CRM:
| Поле | Что значит |
|---|---|
slug | ЧПУ на языке ответа (lang); если для языка ЧПУ нет — румынский |
slugs | ЧПУ на всех языках: { ro, ru, en } — для ссылок на другие языки и hreflang |
- ЧПУ задаётся один раз и не меняется при переименовании объекта, поэтому ссылки и поисковая выдача не ломаются.
- ЧПУ уникален среди объектов компании на каждом языке. Если название совпадает с уже существующим, CRM добавит суффикс (
-2). - Если менеджер всё-таки сменит ЧПУ вручную, прежний адрес попадёт в таблицу переадресаций (ниже) — старая ссылка не даст 404.
Пример маршрута на сайте: /{lang}/apartament/{slug}. Схема адресов — на ваше усмотрение; CRM отдаёт только ЧПУ.
// ссылки на другие языки
<link rel="alternate" hrefLang="ro" href={`https://site.example.com/ro/apartament/${u.slugs.ro}`} />
<link rel="alternate" hrefLang="ru" href={`https://site.example.com/ru/apartament/${u.slugs.ru}`} />Таблица переадресаций: legacyUrls()
GET /legacy-urls отдаёт список старых адресов. Каждый элемент — SiteLegacyUrl:
| Поле | Что значит |
|---|---|
url | старый адрес: страница товара WooCommerce или прежний ЧПУ объекта |
path | путь старого адреса (/ro/product/<slug>/) — ключ для сравнения с запросом |
lang | язык старого адреса |
unit_id | объект |
slug | текущий ЧПУ объекта на языке lang; null — объект удалён |
gone | true — объект удалён |
Что отвечать на запрос старого адреса:
| Случай | Ответ |
|---|---|
объект есть в units() | 301 на /{lang}/apartament/{slug} |
объект в архиве (нет в units(), но gone: false) | 301 на каталог |
gone: true | 410 Gone — страницы больше нет, поисковики уберут её из выдачи |
Сравнивайте путь без завершающего / и без учёта регистра; кириллица в старых ссылках может прийти и в процентной кодировке, и декодированной.
Next.js: redirects() при сборке
Подходит, если старые адреса больше не меняются.
// next.config.ts
import { createSiteClient } from '@type/site-client';
export default {
async redirects() {
const crm = createSiteClient({ baseUrl: process.env.TYPE_API_URL!, apiKey: process.env.TYPE_API_KEY! });
const { units } = await crm.units({ lang: 'ro' });
const live = new Set(units.map((u) => u.id));
return (await crm.legacyUrls())
.filter((l) => !l.gone)
.map((l) => ({
source: l.path,
destination: live.has(l.unit_id) && l.slug ? `/${l.lang}/apartament/${l.slug}` : `/${l.lang}/apartamente`,
permanent: true,
}));
},
};Адреса с gone: true отдайте 410 из middleware или catch-all-маршрута: redirects() умеет только переадресацию.
Next.js: без пересборки
Ведите таблицу в middleware.ts или в catch-all-маршруте (app/product/[...slug]/page.tsx с permanentRedirect()): кэшируйте legacyUrls() через unstable_cache с тегом rental:units и сбрасывайте его вебхуком — новые переадресации появятся без пересборки сайта.
Фото
photos[].url — постоянная ссылка вида /api/modules/rental/site/photos/…. Ключ для неё не нужен, её можно ставить прямо в <img> (через crm.photoUrl(url) — абсолютный адрес). Ссылка отвечает 302 на превью WebP шириной до 1920 px без EXIF, Cache-Control: public, max-age=3000. Если фото удалено или объект в архиве — 404; если превью ещё строится — 503 с Retry-After.