fix(seo): hreflang, open graph, robots and redirect chains #30

Closed
opened 2026-08-07 19:50:49 +03:00 by slaid098 · 0 comments
Owner

Контекст

Сайт slaid098.dev теряет SEO-сигналы: нет in-HTML hreflang тегов (Google не понимает связь ru/en), redirect-цепочки dilute link equity (/youtube → 308 → /yt-video-downloader → 307 → /ru/yt-video-downloader), нет og:image на apps/contacts/subscriptions (плохие social cards), robots.txt не disallow /api/ (crawl budget waste), JSON-LD @id коллизия (home и apps оба WebSite @id=/ru).

Текущее состояние (подтверждено аудитом актуального кода):

  1. Нет hreflang в HTMLgrep "alternates.*languages" по src/ → только src/app/sitemap.ts:53 использует alternates.languages. Ни одна page.tsx не определяет alternates: { languages: {...} } в generateMetadata. Next.js поддерживает это для эмиссии <link rel="alternate" hreflang="ru" ...> в <head>. Sitemap hreflang — вторичный сигнал; primary — in-HTML теги.

  2. Legacy redirect цепочкаnext.config.ts:7-10: /youtubepermanent: true (308) → /yt-video-downloader (без locale). Затем /yt-video-downloader идёт в middleware → 307 → /ru/yt-video-downloader. Итого 2-hop chain (308 → 307). next.config.ts redirects не имеют доступа к Accept-Language. Middleware LEGACY_REDIRECTS (src/middleware.ts:5-8) мог бы редиректить сразу с locale, но это dead code (next.config перехватывает первым). Live: curl -sI https://slaid098.dev/youtube → 308 → /yt-video-downloader.

  3. og:url/og:image missingsrc/app/[locale]/apps/page.tsx:21-27, contacts/page.tsx:20-26, subscriptions/page.tsx:19-25, %5F%5F404%5F%5F/page.tsx:16-19openGraph без images. Layout ([locale]/layout.tsx:44) имеет images: [{ url: "/og-image.png", ... }], но child page's openGraph override'ит родительский → images теряются. Home (page.tsx:24) и [slug] ([slug]/page.tsx:49) имеют images. apps/contacts/subscriptions имеют url, но НЕ имеют images.

  4. robots.txt no disallowsrc/app/robots.ts:7rules: { userAgent: "*", allow: "/" }. Нет disallow для /api/, /_next/. Crawl budget waste.

  5. JSON-LD @id коллизияsrc/app/[locale]/page.tsx:40 (WebSite @id: ${baseUrl}/${loc}) и src/app/[locale]/apps/page.tsx:54 (WebSite @id: ${baseUrl}/${loc}) — одинаковый @id. Schema.org @id должен быть уникальным. Google может merge/confuse entities. Дополнительно: apps/page.tsx:60-66 содержит дублирующий Person JSON-LD (уже есть на home, page.tsx:46-52).

  6. trailingSlash не заданnext.config.ts — нет trailingSlash. Default false, но route regexes принимают optional trailing slash ((?:/)?$) → /ru/apps/ и /ru/apps — потенциально дубли.

Задача

  1. In-HTML hreflang — во ВСЕ generateMetadata функций добавь alternates: { languages: { ru: \${baseUrl}/ru${path}`, en: `${baseUrl}/en${path}`, "x-default": `${baseUrl}/ru${path}` } }гдеpath— путь без locale префикса. Файлы:src/app/[locale]/page.tsx(path=""),src/app/[locale]/apps/page.tsx(path="/apps"),src/app/[locale]/contacts/page.tsx(path="/contacts"),src/app/[locale]/subscriptions/page.tsx(path="/subscriptions"),src/app/[locale]/[slug]/page.tsx (path=/${repo.slug}, только если repo найден). Для [slug]— hreflang на counterpart locale только если repo существует (он существует в обоих locale, same slug). Next.js сгенерируетв<head>`.

  2. Legacy redirect цепочки — убрать legacy redirects из next.config.ts (строки 7-50 для /youtube, /take_break, /support и их locale-вариантов). Перенести в middleware (если LEGACY_REDIRECTS ещё не удалён в issue #29 — используй его; если удалён — добавь обратно живую версию). Middleware имеет getLocale(request) → может редиректить сразу /{locale}/yt-video-downloader с 308. ВАЖНО: coordination с issue #29 — если #29 удаляет LEGACY_REDIRECTS, эта issue должна добавить их обратно как живой код (с 308). Обнови тесты.

  3. og:image — добавь в openGraph для apps/contacts/subscriptions: images: [{ url: "/og-image.png", width: 1024, height: 1024 }] (layout's image, если Next.js merge не работает — добавь явно). Файлы: apps/page.tsx:21, contacts/page.tsx:20, subscriptions/page.tsx:19. og:url уже присутствует во всех трёх.

  4. robots.txt disallowsrc/app/robots.tsrules: { userAgent: "*", allow: "/", disallow: ["/api/", "/*/__404__"] }. Если /__404__ route удалён в issue #29 — disallow только /api/. /_next/ тоже можно disallow (но Next.js сам управляет, опционально).

  5. JSON-LD @id коллизияsrc/app/[locale]/apps/page.tsx:51-59 — измени JSON-LD @type с WebSite на CollectionPage, @id на ${baseUrl}/${loc}/apps, добавь isPartOf: { "@type": "WebSite", "@id": ${baseUrl}/${loc} } (ссылка на home WebSite). Убери дублирующий Person JSON-LD из apps page (строки 60-66) — оставить только на home.

  6. trailingSlashnext.config.ts — добавь trailingSlash: false (явное). Проверь: /ru/apps/ → 308 → /ru/apps (или 404 — уточни поведение Next.js с trailingSlash: false).

Контракты

  • HTML <head> каждой валидной страницы содержит <link rel="alternate" hreflang="ru" href="https://slaid098.dev/ru{path}"> и hreflang="en" и hreflang="x-default"
  • GET /youtube → 308 → /ru/yt-video-downloader (один hop, не два)
  • GET /take_break → 308 → /ru/take-break
  • GET /support → 308 → /ru/contacts
  • GET /ru/youtube → 308 → /ru/yt-video-downloader (один hop)
  • og:url на /ru/apps = https://slaid098.dev/ru/apps
  • og:image на /ru/apps = https://slaid098.dev/og-image.png
  • robots.txt содержит Disallow: /api/
  • JSON-LD на /ru/apps@type: CollectionPage, @id: https://slaid098.dev/ru/apps
  • /ru/apps/ → redirect на /ru/apps (trailingSlash: false)

Инварианты

  • hreflang только для валидных страниц (не для 404)
  • x-default hreflang = ru (default locale)
  • og:url = canonical URL (консистентность)
  • og:image = /og-image.png (для apps/contacts/subscriptions) или repo cover (для [slug])
  • Legacy redirects — 308 (permanent), один hop, с locale префиксом
  • robots.txt Allow: / сохраняется, добавляется Disallow: /api/
  • JSON-LD @id уникальны per page

Граничные случаи

  • Repo существует в одном locale, но не в другом — hreflang должен указывать только на существующие (в текущей модели все репо в обоих locale, но если repo не найден — generateMetadata возвращает {} без hreflang)
  • next.config.ts redirects для locale-префиксированных вариантов (/ru/youtube) — убедиться что middleware ловит их тоже (или оставь в next.config если middleware не может)
  • og:image для [slug] — уже использует repo cover ([slug]/page.tsx:49), НЕ override'ить на /og-image.png
  • x-default hreflang — Google spec: x-default указывает на URL для пользователей без match по locale

Влияние на связанные компоненты

  • src/app/[locale]/page.tsx — hreflang в metadata
  • src/app/[locale]/apps/page.tsx — hreflang + og:image + JSON-LD fix + удаление Person
  • src/app/[locale]/contacts/page.tsx — hreflang + og:image
  • src/app/[locale]/subscriptions/page.tsx — hreflang + og:image
  • src/app/[locale]/[slug]/page.tsx — hreflang (conditional на repo exists)
  • src/app/robots.ts — disallow правила
  • next.config.ts — убрать legacy redirects (перенести в middleware), добавить trailingSlash: false
  • src/middleware.ts — legacy redirects (если #29 удалил, добавить обратно живые с 308) — КООРДИНАЦИЯ с issue #29
  • src/__tests__/middleware.test.ts — тесты legacy redirects

Вне scope

  • Uppercase locale /RU (отдельная issue)
  • manifest.ts (отдельная issue)
  • og:type article (отдельная issue)
  • Sitemap lastmod (отдельная issue)
  • switchLocalePath валидация (отдельная issue)
  • Matcher regex fix (issue #29)
  • 307 → 308 для locale redirects (issue #29)
  • dynamicParams = false (issue #29)
  • /__404__ soft 404 fix (issue #29)

Критерии приемки

  • HTML /ru/apps содержит <link rel="alternate" hreflang="ru" href="https://slaid098.dev/ru/apps">
  • HTML /ru/apps содержит <link rel="alternate" hreflang="en" href="https://slaid098.dev/en/apps">
  • HTML /ru/apps содержит <link rel="alternate" hreflang="x-default" href="https://slaid098.dev/ru/apps">
  • HTML /ru содержит hreflang ru/en/x-default
  • HTML /ru/opencode-config содержит hreflang ru/en (counterpart)
  • curl -sI https://slaid098.dev/youtube → 308 → /ru/yt-video-downloader (один Location header, не цепочка)
  • curl -sI https://slaid098.dev/take_break → 308 → /ru/take-break
  • curl -sI https://slaid098.dev/support → 308 → /ru/contacts
  • HTML /ru/apps meta property og:url = https://slaid098.dev/ru/apps
  • HTML /ru/apps meta property og:image = https://slaid098.dev/og-image.png
  • HTML /ru/contacts meta og:url и og:image присутствуют
  • HTML /ru/subscriptions meta og:url и og:image присутствуют
  • curl -s https://slaid098.dev/robots.txt содержит Disallow: /api/
  • HTML /ru/apps JSON-LD @type = CollectionPage, @id = https://slaid098.dev/ru/apps
  • HTML /ru/apps НЕ содержит Person JSON-LD (только на home)
  • curl -sI https://slaid098.dev/ru/apps/ → redirect на /ru/apps (trailingSlash: false)
  • npm run lint, npm run typecheck, npm test проходят
## Контекст Сайт slaid098.dev теряет SEO-сигналы: нет in-HTML hreflang тегов (Google не понимает связь ru/en), redirect-цепочки dilute link equity (`/youtube` → 308 → `/yt-video-downloader` → 307 → `/ru/yt-video-downloader`), нет og:image на apps/contacts/subscriptions (плохие social cards), robots.txt не disallow `/api/` (crawl budget waste), JSON-LD @id коллизия (home и apps оба `WebSite @id=/ru`). Текущее состояние (подтверждено аудитом актуального кода): 1. **Нет hreflang в HTML** — `grep "alternates.*languages"` по `src/` → только `src/app/sitemap.ts:53` использует `alternates.languages`. Ни одна page.tsx не определяет `alternates: { languages: {...} }` в `generateMetadata`. Next.js поддерживает это для эмиссии `<link rel="alternate" hreflang="ru" ...>` в `<head>`. Sitemap hreflang — вторичный сигнал; primary — in-HTML теги. 2. **Legacy redirect цепочка** — `next.config.ts:7-10`: `/youtube` → `permanent: true` (308) → `/yt-video-downloader` (без locale). Затем `/yt-video-downloader` идёт в middleware → 307 → `/ru/yt-video-downloader`. Итого 2-hop chain (308 → 307). `next.config.ts` redirects не имеют доступа к Accept-Language. Middleware `LEGACY_REDIRECTS` (`src/middleware.ts:5-8`) мог бы редиректить сразу с locale, но это dead code (next.config перехватывает первым). Live: `curl -sI https://slaid098.dev/youtube` → 308 → `/yt-video-downloader`. 3. **og:url/og:image missing** — `src/app/[locale]/apps/page.tsx:21-27`, `contacts/page.tsx:20-26`, `subscriptions/page.tsx:19-25`, `%5F%5F404%5F%5F/page.tsx:16-19` — `openGraph` без `images`. Layout (`[locale]/layout.tsx:44`) имеет `images: [{ url: "/og-image.png", ... }]`, но child page's `openGraph` override'ит родительский → images теряются. Home (`page.tsx:24`) и `[slug]` (`[slug]/page.tsx:49`) имеют images. apps/contacts/subscriptions имеют `url`, но НЕ имеют `images`. 4. **robots.txt no disallow** — `src/app/robots.ts:7` — `rules: { userAgent: "*", allow: "/" }`. Нет `disallow` для `/api/`, `/_next/`. Crawl budget waste. 5. **JSON-LD @id коллизия** — `src/app/[locale]/page.tsx:40` (WebSite `@id: ${baseUrl}/${loc}`) и `src/app/[locale]/apps/page.tsx:54` (WebSite `@id: ${baseUrl}/${loc}`) — одинаковый `@id`. Schema.org `@id` должен быть уникальным. Google может merge/confuse entities. Дополнительно: `apps/page.tsx:60-66` содержит дублирующий `Person` JSON-LD (уже есть на home, `page.tsx:46-52`). 6. **trailingSlash не задан** — `next.config.ts` — нет `trailingSlash`. Default `false`, но route regexes принимают optional trailing slash (`(?:/)?$`) → `/ru/apps/` и `/ru/apps` — потенциально дубли. ## Задача 1. **In-HTML hreflang** — во ВСЕ `generateMetadata` функций добавь `alternates: { languages: { ru: \`${baseUrl}/ru${path}\`, en: \`${baseUrl}/en${path}\`, "x-default": \`${baseUrl}/ru${path}\` } }` где `path` — путь без locale префикса. Файлы: `src/app/[locale]/page.tsx` (path=""), `src/app/[locale]/apps/page.tsx` (path="/apps"), `src/app/[locale]/contacts/page.tsx` (path="/contacts"), `src/app/[locale]/subscriptions/page.tsx` (path="/subscriptions"), `src/app/[locale]/[slug]/page.tsx` (path=`/${repo.slug}`, только если repo найден). Для `[slug]` — hreflang на counterpart locale только если repo существует (он существует в обоих locale, same slug). Next.js сгенерирует `<link rel="alternate" hreflang="ru" href="...">` в `<head>`. 2. **Legacy redirect цепочки** — убрать legacy redirects из `next.config.ts` (строки 7-50 для `/youtube`, `/take_break`, `/support` и их locale-вариантов). Перенести в middleware (если `LEGACY_REDIRECTS` ещё не удалён в issue #29 — используй его; если удалён — добавь обратно живую версию). Middleware имеет `getLocale(request)` → может редиректить сразу `/{locale}/yt-video-downloader` с 308. ВАЖНО: coordination с issue #29 — если #29 удаляет `LEGACY_REDIRECTS`, эта issue должна добавить их обратно как живой код (с 308). Обнови тесты. 3. **og:image** — добавь в `openGraph` для apps/contacts/subscriptions: `images: [{ url: "/og-image.png", width: 1024, height: 1024 }]` (layout's image, если Next.js merge не работает — добавь явно). Файлы: `apps/page.tsx:21`, `contacts/page.tsx:20`, `subscriptions/page.tsx:19`. og:url уже присутствует во всех трёх. 4. **robots.txt disallow** — `src/app/robots.ts` — `rules: { userAgent: "*", allow: "/", disallow: ["/api/", "/*/__404__"] }`. Если `/__404__` route удалён в issue #29 — disallow только `/api/`. `/_next/` тоже можно disallow (но Next.js сам управляет, опционально). 5. **JSON-LD @id коллизия** — `src/app/[locale]/apps/page.tsx:51-59` — измени JSON-LD `@type` с `WebSite` на `CollectionPage`, `@id` на `${baseUrl}/${loc}/apps`, добавь `isPartOf: { "@type": "WebSite", "@id": ${baseUrl}/${loc} }` (ссылка на home WebSite). Убери дублирующий `Person` JSON-LD из apps page (строки 60-66) — оставить только на home. 6. **trailingSlash** — `next.config.ts` — добавь `trailingSlash: false` (явное). Проверь: `/ru/apps/` → 308 → `/ru/apps` (или 404 — уточни поведение Next.js с `trailingSlash: false`). ## Контракты - HTML `<head>` каждой валидной страницы содержит `<link rel="alternate" hreflang="ru" href="https://slaid098.dev/ru{path}">` и `hreflang="en"` и `hreflang="x-default"` - `GET /youtube` → 308 → `/ru/yt-video-downloader` (один hop, не два) - `GET /take_break` → 308 → `/ru/take-break` - `GET /support` → 308 → `/ru/contacts` - `GET /ru/youtube` → 308 → `/ru/yt-video-downloader` (один hop) - og:url на `/ru/apps` = `https://slaid098.dev/ru/apps` - og:image на `/ru/apps` = `https://slaid098.dev/og-image.png` - robots.txt содержит `Disallow: /api/` - JSON-LD на `/ru/apps` — `@type: CollectionPage`, `@id: https://slaid098.dev/ru/apps` - `/ru/apps/` → redirect на `/ru/apps` (trailingSlash: false) ## Инварианты - hreflang только для валидных страниц (не для 404) - `x-default` hreflang = `ru` (default locale) - og:url = canonical URL (консистентность) - og:image = `/og-image.png` (для apps/contacts/subscriptions) или repo cover (для [slug]) - Legacy redirects — 308 (permanent), один hop, с locale префиксом - robots.txt `Allow: /` сохраняется, добавляется `Disallow: /api/` - JSON-LD `@id` уникальны per page ## Граничные случаи - Repo существует в одном locale, но не в другом — hreflang должен указывать только на существующие (в текущей модели все репо в обоих locale, но если repo не найден — `generateMetadata` возвращает `{}` без hreflang) - `next.config.ts` redirects для locale-префиксированных вариантов (`/ru/youtube`) — убедиться что middleware ловит их тоже (или оставь в next.config если middleware не может) - og:image для `[slug]` — уже использует repo cover (`[slug]/page.tsx:49`), НЕ override'ить на `/og-image.png` - x-default hreflang — Google spec: `x-default` указывает на URL для пользователей без match по locale ## Влияние на связанные компоненты - `src/app/[locale]/page.tsx` — hreflang в metadata - `src/app/[locale]/apps/page.tsx` — hreflang + og:image + JSON-LD fix + удаление Person - `src/app/[locale]/contacts/page.tsx` — hreflang + og:image - `src/app/[locale]/subscriptions/page.tsx` — hreflang + og:image - `src/app/[locale]/[slug]/page.tsx` — hreflang (conditional на repo exists) - `src/app/robots.ts` — disallow правила - `next.config.ts` — убрать legacy redirects (перенести в middleware), добавить `trailingSlash: false` - `src/middleware.ts` — legacy redirects (если #29 удалил, добавить обратно живые с 308) — КООРДИНАЦИЯ с issue #29 - `src/__tests__/middleware.test.ts` — тесты legacy redirects ## Вне scope - ❌ Uppercase locale `/RU` (отдельная issue) - ❌ manifest.ts (отдельная issue) - ❌ og:type article (отдельная issue) - ❌ Sitemap lastmod (отдельная issue) - ❌ switchLocalePath валидация (отдельная issue) - ❌ Matcher regex fix (issue #29) - ❌ 307 → 308 для locale redirects (issue #29) - ❌ dynamicParams = false (issue #29) - ❌ `/__404__` soft 404 fix (issue #29) ## Критерии приемки - [ ] HTML `/ru/apps` содержит `<link rel="alternate" hreflang="ru" href="https://slaid098.dev/ru/apps">` - [ ] HTML `/ru/apps` содержит `<link rel="alternate" hreflang="en" href="https://slaid098.dev/en/apps">` - [ ] HTML `/ru/apps` содержит `<link rel="alternate" hreflang="x-default" href="https://slaid098.dev/ru/apps">` - [ ] HTML `/ru` содержит hreflang ru/en/x-default - [ ] HTML `/ru/opencode-config` содержит hreflang ru/en (counterpart) - [ ] `curl -sI https://slaid098.dev/youtube` → 308 → `/ru/yt-video-downloader` (один Location header, не цепочка) - [ ] `curl -sI https://slaid098.dev/take_break` → 308 → `/ru/take-break` - [ ] `curl -sI https://slaid098.dev/support` → 308 → `/ru/contacts` - [ ] HTML `/ru/apps` meta property `og:url` = `https://slaid098.dev/ru/apps` - [ ] HTML `/ru/apps` meta property `og:image` = `https://slaid098.dev/og-image.png` - [ ] HTML `/ru/contacts` meta `og:url` и `og:image` присутствуют - [ ] HTML `/ru/subscriptions` meta `og:url` и `og:image` присутствуют - [ ] `curl -s https://slaid098.dev/robots.txt` содержит `Disallow: /api/` - [ ] HTML `/ru/apps` JSON-LD `@type` = `CollectionPage`, `@id` = `https://slaid098.dev/ru/apps` - [ ] HTML `/ru/apps` НЕ содержит `Person` JSON-LD (только на home) - [ ] `curl -sI https://slaid098.dev/ru/apps/` → redirect на `/ru/apps` (trailingSlash: false) - [ ] `npm run lint`, `npm run typecheck`, `npm test` проходят
Sign in to join this conversation.
No labels
bug
performance
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
slaid098/slaid098-dev#30
No description provided.