fix(seo): locale edge cases manifest and structured data polish #34

Merged
slaid098 merged 4 commits from fix/seo/locale-edge-cases-manifest-polish into main 2026-08-07 20:47:13 +03:00
Owner

Что сделано

  • Case-insensitive locale detection в middleware (src/middleware.ts) — /RU, /En, /EN/foo → 308 redirect на lowercase-вариант с сохранением path. Matcher regex не менялся (uppercase не матчит ru|en в lookahead → middleware срабатывает и делает case-correction). 8 новых тестов в middleware.test.ts.
  • manifest.ts start_url"/""/ru" (default locale, PWA launch без redirect hop). lang: "ru" оставлен (single manifest, locale-aware — out of scope).
  • [slug]/page.tsx og:type"website""article" + publishedTime: repo.createdAt + authors: ["Sergey"] для rich social cards.
  • Sitemap lastmodDiscoveredRepo и ForgejoRepoDetail получили updatedAt/updated_at поле, проброшено через buildDiscoveredRepo/makeErrorRepo/discoverShowcaseRepos. Sitemap: lastmod = updatedAt ?? createdAt (fallback на createdAt если updated_at null).
  • switchLocalePath — JSDoc-документация инварианта same-slug-both-locales (функция осталась sync pure, async validation — out of scope).
  • Root layout.tsx metadataBasenew URL(baseUrl) для root not-found.tsx metadata URL resolution.

Почему

Issue #31 — остаточные SEO/UX edge cases после PR#32 (routing/404) и PR#33 (hreflang/OG/robots). Uppercase locale bypass, hardcoded manifest lang/start_url, og:type=website для detail-страниц, sitemap lastmod=createdAt (не обновляется), switchLocalePath без валидации, root metadataBase missing. По отдельности low-severity, но для цели «ни одного косяка» — все закрыты.

Watch out

  • Спека пункта 1 содержала баг в формуле redirect target (pathname.slice(first.length) не учитывала ведущий / — давало /enN/foo вместо /en/foo). Исправлено: redirect target собирается из segments (/${lowerFirst}/${rest}), не из slice.
  • ForgejoRepoListItem тип получил updated_at: string | null — mock-объекты в 5 тестах обновлены (updatedAt: null), mock RepoDetail в showcase-single-repo.test.ts использует as-каст и не требует обновления (undefined → fallback null).

Pending

Closes #31

## Что сделано - **Case-insensitive locale detection в middleware** (`src/middleware.ts`) — `/RU`, `/En`, `/EN/foo` → 308 redirect на lowercase-вариант с сохранением path. Matcher regex не менялся (uppercase не матчит `ru|en` в lookahead → middleware срабатывает и делает case-correction). 8 новых тестов в `middleware.test.ts`. - **`manifest.ts` `start_url`** — `"/"` → `"/ru"` (default locale, PWA launch без redirect hop). `lang: "ru"` оставлен (single manifest, locale-aware — out of scope). - **`[slug]/page.tsx` og:type** — `"website"` → `"article"` + `publishedTime: repo.createdAt` + `authors: ["Sergey"]` для rich social cards. - **Sitemap `lastmod`** — `DiscoveredRepo` и `ForgejoRepoDetail` получили `updatedAt`/`updated_at` поле, проброшено через `buildDiscoveredRepo`/`makeErrorRepo`/`discoverShowcaseRepos`. Sitemap: `lastmod = updatedAt ?? createdAt` (fallback на createdAt если updated_at null). - **`switchLocalePath`** — JSDoc-документация инварианта same-slug-both-locales (функция осталась sync pure, async validation — out of scope). - **Root `layout.tsx` `metadataBase`** — `new URL(baseUrl)` для root `not-found.tsx` metadata URL resolution. ## Почему Issue #31 — остаточные SEO/UX edge cases после PR#32 (routing/404) и PR#33 (hreflang/OG/robots). Uppercase locale bypass, hardcoded manifest lang/start_url, og:type=website для detail-страниц, sitemap lastmod=createdAt (не обновляется), switchLocalePath без валидации, root metadataBase missing. По отдельности low-severity, но для цели «ни одного косяка» — все закрыты. ## Watch out - Спека пункта 1 содержала баг в формуле redirect target (`pathname.slice(first.length)` не учитывала ведущий `/` — давало `/enN/foo` вместо `/en/foo`). Исправлено: redirect target собирается из segments (`/${lowerFirst}/${rest}`), не из slice. - `ForgejoRepoListItem` тип получил `updated_at: string | null` — mock-объекты в 5 тестах обновлены (`updatedAt: null`), mock `RepoDetail` в `showcase-single-repo.test.ts` использует `as`-каст и не требует обновления (undefined → fallback null). ## Pending — Closes #31
Author
Owner

Code Review Summary

Качественный SEO-polish PR: case-insensitive locale redirect в middleware, manifest start_url, og:type=article для detail-страниц, sitemap lastmod с fallback на createdAt, root metadataBase, JSDoc для switchLocalePath. Все 197 тестов зелёные, tsc --noEmit чисто, 5 mock-объектов обновлены с updatedAt: null. Исправление бага спеки (segment slicing → join) корректно и задокументировано в Watch out.

Positives

  • Middleware case-correction реализована элегантно: использует существующий matcher (uppercase не матчит ru|en lookahead → middleware срабатывает), redirect target собирается из segments (не slice), 308 permanent. 8 тестов покрывают /RU, /En, /Ru, /RU/apps, /En/foo, /EN/foo + negative cases (already-lowercase passes through).
  • DiscoveredRepo.updatedAt проброшен консистентно через весь стек: ForgejoRepoListItemRepoMetabuildDiscoveredRepo/makeErrorRepodiscoverShowcaseRepos. Sitemap fallback updatedAt ?? createdAt с NaN-guard — корректный.
  • JSDoc для switchLocalePath документирует нетривиальный инвариант (same-slug-both-locales) — соответствует code-standards §4 (public API contract warrants docstring).
  • Root layout.tsx metadataBase не дублирует [locale]/layout.tsx — Next.js использует nearest, root применяется только к not-found.tsx вне [locale] segment. Корректное решение.
  • PR body качественный: ## Что сделано детально, ## Почему ссылается на issue #31, ## Watch out описывает fix бага спеки + mock-обновления.

Suggestions (info, not blocking)

  • src/lib/readme-discovery.ts:217,237 [consistency] buildDiscoveredRepo ставит updatedAt: repo.updated_at без ?? null, тогда как makeErrorRepo (line 186) и discoverShowcaseRepos (line 313) нормализуют ?? null. Путь discoverSingleRepo (line 277) передаёт detail с updated_at: undefined в рантайме (моки в showcase-single-repo.test.ts:49-60 и generate-metadata.test.ts:80-89 не включают updated_at) → updatedAt: undefined вместо null, нарушая тип string | null. Runtime-безопасно (sitemap трактует falsy), но нарушает паттерн из того же PR. Fix: updatedAt: repo.updated_at ?? null.

  • src/app/manifest.ts:8 [DRY] start_url: "/ru" хардкод вместо DEFAULT_LOCALE. Если DEFAULT_LOCALE сменится на "en", start_url устареет. Fix: import { DEFAULT_LOCALE } from "@/lib/i18n"start_url: \/${DEFAULT_LOCALE}``.

  • src/tests/middleware.test.ts [test coverage] Нет теста, что matcher regex всё ещё матчит uppercase пути (/RU, /En). Если кто-то изменит matcher, чтобы исключать uppercase, case-correction станет dead code, а существующие 8 тестов пройдут (они вызывают middleware напрямую, минуя matcher). Fix: добавить expect(matches("/RU")).toBe(true), expect(matches("/En")).toBe(true) в matcher describe block.

  • src/app/[locale]/[slug]/page.tsx:57-58 [suggestion] Добавлен publishedTime: repo.createdAt, но не modifiedTime: repo.updatedAt. Для og:article modifiedTime улучшает social card caching. Fix: modifiedTime: repo.updatedAt ?? repo.createdAt.

Verdict: APPROVE

## Code Review Summary Качественный SEO-polish PR: case-insensitive locale redirect в middleware, manifest start_url, og:type=article для detail-страниц, sitemap lastmod с fallback на createdAt, root metadataBase, JSDoc для switchLocalePath. Все 197 тестов зелёные, tsc --noEmit чисто, 5 mock-объектов обновлены с `updatedAt: null`. Исправление бага спеки (segment slicing → join) корректно и задокументировано в Watch out. ### Positives - Middleware case-correction реализована элегантно: использует существующий matcher (uppercase не матчит `ru|en` lookahead → middleware срабатывает), redirect target собирается из segments (не slice), 308 permanent. 8 тестов покрывают /RU, /En, /Ru, /RU/apps, /En/foo, /EN/foo + negative cases (already-lowercase passes through). - `DiscoveredRepo.updatedAt` проброшен консистентно через весь стек: `ForgejoRepoListItem` → `RepoMeta` → `buildDiscoveredRepo`/`makeErrorRepo` → `discoverShowcaseRepos`. Sitemap fallback `updatedAt ?? createdAt` с NaN-guard — корректный. - JSDoc для `switchLocalePath` документирует нетривиальный инвариант (same-slug-both-locales) — соответствует code-standards §4 (public API contract warrants docstring). - Root `layout.tsx` `metadataBase` не дублирует `[locale]/layout.tsx` — Next.js использует nearest, root применяется только к `not-found.tsx` вне `[locale]` segment. Корректное решение. - PR body качественный: `## Что сделано` детально, `## Почему` ссылается на issue #31, `## Watch out` описывает fix бага спеки + mock-обновления. ### Suggestions (info, not blocking) - **src/lib/readme-discovery.ts:217,237** [consistency] `buildDiscoveredRepo` ставит `updatedAt: repo.updated_at` без `?? null`, тогда как `makeErrorRepo` (line 186) и `discoverShowcaseRepos` (line 313) нормализуют `?? null`. Путь `discoverSingleRepo` (line 277) передаёт `detail` с `updated_at: undefined` в рантайме (моки в `showcase-single-repo.test.ts:49-60` и `generate-metadata.test.ts:80-89` не включают `updated_at`) → `updatedAt: undefined` вместо `null`, нарушая тип `string | null`. Runtime-безопасно (sitemap трактует falsy), но нарушает паттерн из того же PR. Fix: `updatedAt: repo.updated_at ?? null`. - **src/app/manifest.ts:8** [DRY] `start_url: "/ru"` хардкод вместо `DEFAULT_LOCALE`. Если `DEFAULT_LOCALE` сменится на `"en"`, `start_url` устареет. Fix: `import { DEFAULT_LOCALE } from "@/lib/i18n"` → `start_url: \`/${DEFAULT_LOCALE}\``. - **src/__tests__/middleware.test.ts** [test coverage] Нет теста, что matcher regex всё ещё матчит uppercase пути (`/RU`, `/En`). Если кто-то изменит matcher, чтобы исключать uppercase, case-correction станет dead code, а существующие 8 тестов пройдут (они вызывают middleware напрямую, минуя matcher). Fix: добавить `expect(matches("/RU")).toBe(true)`, `expect(matches("/En")).toBe(true)` в matcher describe block. - **src/app/[locale]/[slug]/page.tsx:57-58** [suggestion] Добавлен `publishedTime: repo.createdAt`, но не `modifiedTime: repo.updatedAt`. Для og:article `modifiedTime` улучшает social card caching. Fix: `modifiedTime: repo.updatedAt ?? repo.createdAt`. ### Verdict: APPROVE
slaid098 deleted branch fix/seo/locale-edge-cases-manifest-polish 2026-08-07 20:47:14 +03:00
Sign in to join this conversation.
No reviewers
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!34
No description provided.