**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 щотижня `redirect-chain-verify` HEAD-трасує редиректи та позначає ланцюги і зламані цілі.