---
title: "Динамическое формирование ключей кэша на границе для обеспечения SEO‑последовательности в многоязычных сайтах"
---

# Динамическое формирование ключей кэша на границе для обеспечения SEO‑последовательности в многоязычных сайтах

Когда веб‑сайт обслуживает один и тот же контент на нескольких языках, слой edge становится критической точкой управления. Хорошо продуманная стратегия ключей кэша может мгновенно доставлять нужную языковую версию **и** сохранять доверие поисковых систем к сайту. В этой статье мы разберём, почему традиционные статические ключи не подходят, как работает динамическое составление ключей на границе и какие шаблоны позволяют сохранять сигналы [SEO](https://moz.com/learn/seo/what-is-seo) в разных регионах.

## Проблема «один размер подходит всем» для ключей

Большинство CDN‑провайдеров по умолчанию используют простой хеш запрашиваемого URL. Это работает для монолитных (одноязычных) сайтов, но когда пользователь запрашивает `/en/about`, а другой — `/fr/about`, хеш отличается лишь языковым сегментом. CDN сохраняет два отдельные объекта, что, в принципе, нормально, однако такой ключ всё равно не учитывает нюансы **hreflang** и **canonical**. Поисковые системы рассматривают эти объекты как отдельные ресурсы; если одна и та же страница кэшируется под несколькими ключами без корректных сигналы, могут возникнуть штрафы за дублирующий контент, а бюджет обхода (crawl budget) будет тратиться впустую на повторную загрузку одинаковых ресурсов.

## Основные компоненты многоязычного ключа кэша

Надёжный многоязычный ключ кэша обычно комбинирует следующие измерения:

* **Код языка** — извлекается из пути URL, субдомена или заголовка `Accept-Language`.
* **Подсказка о регионе** — опционально, берётся из данных GeoIP, чтобы учитывать региональные различия (валюта, единицы измерения).
* **Класс устройства** — мобильный vs. десктоп, важно для адаптивного дизайна.
* **Вариативные заголовки запроса** — например `Accept-Encoding` для gzip vs. brotli или `User‑Agent` для A/B‑тестов.

Эти измерения конкатенируются в детерминированном порядке и хешируются только после нормализации. В результате получается лёгкая строка, которую edge‑сервер использует для поиска, при этом полностью отражая контекст запроса.

## Конвейер нормализации на границе

Прежде чем собрать ключ, edge‑функция должна нормализовать запрос:

1. **Убрать параметры слежения** — удалить строки запроса `utm_*`, чтобы аналитические метки не фрагментировали кэш.
2. **Канонизировать путь** — устранить дублирующиеся слеши, относительные сегменты (`../`) и завершающие слеши в соответствии с политикой URL сайта.
3. **Сопоставить алиасы языков** — считать `/en-us` и `/en` эквивалентными, если сайт обслуживает одну английскую версию.
4. **Привести к нижнему регистру** — URL обычно нечувствительны к регистру; принудительное приведение к нижнему устраняет лишние варианты.

Нормализованный запрос затем передаётся генератору ключа. Ниже — упрощённый пример псевдокода, который может работать в Cloudflare Workers, AWS Lambda@Edge или любой другой V8‑базированной среде edge.

```javascript
// edge-key-generator.js
export async function handle(event) {
  const { request } = event;
  const url = new URL(request.url);

  // 1️⃣ Remove analytics parameters
  for (const param of url.searchParams.keys()) {
    if (param.startsWith('utm_')) url.searchParams.delete(param);
  }

  // 2️⃣ Normalize pathname
  url.pathname = decodeURI(url.pathname)
                    .replace(/\/{2,}/g, '/')
                    .replace(/\/$/, '');

  // 3️⃣ Detect language from path or header
  const langMatch = url.pathname.match(/^\/([a-z]{2})(?:-[A-Z]{2})?(?=\/|$)/);
  const language = langMatch ? langMatch[1] : request.headers.get('Accept-Language')?.split(',')[0]?.slice(0,2) || 'en';

  // 4️⃣ Build key components
  const region = request.headers.get('CF-IPCountry') || 'XX';
  const device = request.headers.get('User-Agent')?.includes('Mobile') ? 'mobile' : 'desktop';

  // 5️⃣ Assemble deterministic key
  const rawKey = `${language}|${region}|${device}|${url.pathname}`;
  const cacheKey = crypto.subtle.digest('SHA-256', new TextEncoder().encode(rawKey))
                    .then(buf => [...new Uint8Array(buf)].map(b => b.toString(16).padStart(2, '0')).join(''));

  // Attach the cache key for downstream caching logic
  request.headers.set('X-Edge-Cache-Key', await cacheKey);
  return request;
}