**TL;DR.** Перенаправления в электронной коммерции направлены на сохранение SEO-активов при изменении URL. Используйте 301 для постоянных перемещений (исправления слагов, ребрендинг). Используйте 410 для постоянных удалений. Отслеживайте историю слагов для каждой сущности, чтобы старые URL автоматически перенаправлялись. Обнаруживайте и упрощайте цепочки раз в неделю. Отображайте отсутствующие страницы с полезным контентом, а не общим 404.

## HTTP статус-коды для электронной коммерции

| Код  | Название           | Значение                       | Случай использования                               |
| ---- | ------------------ | ------------------------------ | ------------------------------------------------- |
| 200  | OK                 | Страница существует            | Обычная страница продукта/категории               |
| 301  | Перемещено навсегда| Используйте новый URL навсегда | Изменения слагов, ребрендинг, миграции доменов    |
| 302  | Найден             | Временное перенаправление     | A/B тесты, гео-перенаправления, временные сбои    |
| 307  | Временное перенаправление | То же, что и 302, но сохраняет метод | API перенаправления с POST                     |
| 308  | Постоянное перенаправление | То же, что и 301, но сохраняет метод | API перенаправления с POST                     |
| 404  | Не найден          | Страница не существует, может вернуться | Ошибки, неправильно запомненные URL          |
| 410  | Удалено            | Удалено навсегда              | Прекращенные продукты, удаленный контент          |

Для электронной коммерции общая пара — это **301** для перемещений и **410** для постоянных удалений.

## Когда использовать 301

Распространенные сценарии 301:

| Событие                        | Шаблон                                              |
| ------------------------------ | ---------------------------------------------------- |
| Исправление слага продукта (опечатка) | `/products/leather-bagg` → `/products/leather-bag`   |
| Ребрендинг продукта            | `/products/leather-bag` → `/products/heritage-bag`    |
| Реструктуризация категории     | `/products/bags-leather` → `/categories/leather-bags` |
| Миграция домена               | `oldbrand.com/...` → `newbrand.com/...`               |
| Изменение URL локали           | `/products/bag` → `/en/products/bag`                  |
| Нормализация завершающего слэша | `/products/bag/` → `/products/bag`                    |
| HTTP → HTTPS                   | `http://...` → `https://...`                          |

Реализация в Next.js:

```ts
// next.config.ts
export default {
  async redirects() {
    return [
      {
        source: '/products/leather-bagg',
        destination: '/products/leather-bag',
        permanent: true,  // 301
      },
    ];
  },
};
```

Или через таблицу перенаправлений во время выполнения:

```ts
// src/middleware.ts or proxy.ts
const redirect = await getRedirect(request.url);
if (redirect) {
  return NextResponse.redirect(redirect.to, redirect.statusCode);
}
```

## Когда использовать 410

410 Gone — это правильный ответ, когда:

- Продукт навсегда прекращен.
- Запись в блоге удалена по причинам точности или юридическим причинам.
- Категория была объединена с другой, и старый слаг должен быть удален навсегда.
- Страница намеренно удалена и не вернется.

```ts
// Псевдокод
const gone = await isGonePath(storeId, request.url.pathname);
if (gone) {
  return new Response(renderGonePage(gone.suggestions), {
    status: 410,
    headers: { 'Content-Type': 'text/html' },
  });
}
```

Страница 410 должна:

- Отображать `<meta name="robots" content="noindex">`.
- Предлагать похожие продукты (используйте pgvector cosine на встраивании исчезнувшей сущности).
- Иметь поле для поиска.
- Содержать четкое сообщение "этот продукт больше недоступен".

## История слагов для каждой сущности

Когда слаг продукта изменяется, старый URL должен перенаправляться на новый URL. Хранение этого вручную ненадежно. Отслеживайте историю слагов для каждой сущности:

```sql
CREATE TABLE product_slug_history (
  id SERIAL PRIMARY KEY,
  product_id UUID NOT NULL,
  old_slug TEXT NOT NULL,
  new_slug TEXT NOT NULL,
  changed_at TIMESTAMPTZ DEFAULT NOW()
);
```

При обновлении слага:

```ts
async function updateProductSlug(productId: string, newSlug: string) {
  const product = await db.products.findOne({ id: productId });
  const oldSlug = product.slug;

  if (oldSlug === newSlug) return;

  await db.products.update({ id: productId, slug: newSlug });
  await db.productSlugHistory.insert({ productId, oldSlug, newSlug });
  await db.storeRedirects.insert({
    from: `/products/${oldSlug}`,
    to: `/products/${newSlug}`,
    statusCode: 301,
  });
  await enqueueIndexNow([`/products/${oldSlug}`, `/products/${newSlug}`]);
}
```

Таблица истории слагов позволяет отслеживать эволюцию URL продукта (полезно для поддержки и для юридических/соответствующих архивов).

## Цепочки перенаправлений

Цепочка: `A → B → C`.

Почему цепочки плохи:

- Каждое пересечение теряет 5–10% ссылочного капитала (Google это подтвердил).
- Каждое пересечение добавляет 100–500 мс задержки.
- Три или более пересечения увеличивают вероятность того, что Google перестанет отслеживать.

Обнаружение: HEAD-трассировка каждого перенаправления раз в неделю.

```ts
async function traceRedirect(url: string, hops: number = 0): Promise<{ finalUrl: string; chainLength: number }> {
  if (hops > 5) return { finalUrl: url, chainLength: hops }; // защита от зацикливания

  const res = await fetch(url, { method: 'HEAD', redirect: 'manual' });
  if (res.status >= 300 && res.status < 400) {
    const location = res.headers.get('location');
    if (!location) return { finalUrl: url, chainLength: hops };
    return traceRedirect(location, hops + 1);
  }
  return { finalUrl: url, chainLength: hops };
}
```

Упрощение цепочек: перепишите источник, чтобы он указывал непосредственно на конечный URL.

```ts
// До: A → B → C
await db.redirects.update({ from: '/a', to: '/b' });  // существующее
// После упрощения:
await db.redirects.update({ from: '/a', to: '/c' });  // переписано
```

## Циклы перенаправлений

`A → B → A` фатален. Обнаружьте это во время записи:

```ts
async function writeRedirect(from: string, to: string) {
  const wouldLoop = await detectLoop(from, to);
  if (wouldLoop) {
    throw new Error(`Перенаправление с ${from} на ${to} создает цикл`);
  }
  await db.redirects.insert({ from, to, statusCode: 301 });
}
```

## Пинг IndexNow

После записи перенаправления:

```ts
await enqueueIndexNow([oldUrl, newUrl]);
```

Движок повторно извлекает старый URL, видит 301 и обновляет свой индекс.

## Лучшие практики

- Держите таблицу перенаправлений компактной. Со временем десятки небольших изменений слагов создают десятки перенаправлений. Периодически упрощайте цепочки.
- Не перенаправляйте целые категории на главную страницу. Это "мягкий 404" для Google. Перенаправляйте на ближайшую эквивалентную категорию.
- Избегайте перенаправления на страницы noindex. Это противоречит цели сохранения капитала.
- Документируйте намерение перенаправления. Комментарий в столбце таблицы перенаправлений поможет вам в будущем понять, почему.

## Как Ordiko обрабатывает перенаправления

- Таблица `storeRedirects` с колонками `from`, `to`, `statusCode`, `tenant`.
- История слагов для каждой сущности: `productSlugHistory`, `categorySlugHistory` и т.д.
- Автоматическая запись 301 при изменении слага.
- `productLifecycle.archive()` записывает строку 410 в `storeRedirects`.
- Слой исчезнувших путей отображает `/products/[slug]/gone` с рекомендациями по схожим продуктам.
- Задача Trigger.dev `redirect-chain-verify.task.ts` раз в неделю HEAD-трассирует каждое перенаправление и помечает цепочки и сломанные цели.
- Одно нажатие "Упрощение цепочки" переписывает A → C напрямую.
- Пинги IndexNow при каждом создании перенаправления.

## FAQ

**В чем практическая разница между 301 и 302?**
301 = навсегда. Передает почти весь ссылочный капитал. Сильно кэширует перенаправление. Используйте для изменений слагов, миграций доменов. 302 = временно. Передает меньше капитала. Не кэширует так агрессивно. Используйте для A/B тестов или временных сбоев. Для большинства перенаправлений в электронной коммерции 301 является правильным.

**В чем разница между 404 и 410?**
404 = "не найден, может вернуться". Google может некоторое время хранить URL в своем индексе на случай, если он вернется. 410 = "удалено, навсегда". Google быстрее удаляет URL. Для продуктов, которые были навсегда прекращены, 410 лучше с точки зрения SEO.

**Сколько пересечений перенаправлений слишком много?**
Два пересечения — это практический предел. Каждое пересечение теряет 5–10% ссылочного капитала и добавляет задержку. Три или более пересечения — это сигнал. Обнаруживайте и упрощайте их автоматически.

**Как Ordiko обрабатывает историю слагов?**
Каждая таблица сущности имеет соответствующую таблицу истории слагов (например, product_slug_history). При изменении слага новая строка вставляется со старым слагом; 301 перенаправление автоматически записывается в storeRedirects. Задача проверки цепочек перенаправлений Trigger.dev раз в неделю HEAD-трассирует перенаправления и помечает цепочки и сломанные цели.