fix(seo): routing and 404 status codes #32
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/seo/routing-and-404-codes"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Что сделано
Исправлены 5 SEO-багов из issue #29:
Matcher граница locale-сегмента (
src/middleware.ts) — negative lookahead|ru|enзаменён на(?!(?:ru|en)(?:/|$)). Теперь/rusds12312,/enfoo,/russia,/energy— middleware РАБОТАЕТ → редирект на/ru/rusds12312(далее hard 404 черезdynamicParams=false)./ru,/ru/apps,/en/foo— исключены. Проверено regex-тестом (17 кейсов, все pass).Редиректы 308 permanent (
src/middleware.ts) — всеNextResponse.redirect(new URL(...))→NextResponse.redirect(new URL(...), 308). Тесты обновлены: 307 → 308 (8 кейсов).dynamicParams = false(src/app/[locale]/layout.tsx) — unknown locale (/de,/fr,/xyz) → hard 404 (через static generation, без рендера home). Build подтверждает:/[locale]SSG prerendered только для/ru,/en.Удалён soft-404 route (
src/app/[locale]/%5F%5F404%5F%5F/page.tsx) —/__404__отдавал HTTP 200 (textbook soft 404). Подтверждено через context7 (Next.js 15.1.8 docs):notFound()выставляет HTTP 404 черезNEXT_HTTP_ERROR_FALLBACK;404digest → server setsstatusCode=404. Route удалён;notFound()в[slug]/page.tsx:70теперь отдаёт hard 404 с локализованным контентом через[locale]/not-found.tsx.Canonical использует реальный locale (
src/app/[locale]/page.tsx) —isLocale(locale) ? locale : DEFAULT_LOCALE→if (!isLocale(locale)) notFound(); const loc = locale. Defense-in-depth: fallback на DEFAULT_LOCALE убран, canonical всегда реальный сегмент. ПослеdynamicParams=falsefallback-кейс недостижим, но guard защищает от регрессии.Удалён dead code (
src/middleware.ts) —LEGACY_REDIRECTSдублировалnext.config.tsredirects (unreachable: next.config redirects запускаются BEFORE middleware). Связанные тесты удалены (блок «middleware legacy redirects»).Root not-found.tsx (
src/app/not-found.tsx) —redirect(/${DEFAULT_LOCALE}/__404__)заменён на рендер локализованного 404 на основеAccept-Language(черезheaders()). Root not-found не участвует в ISR (рендерится только при 404), поэтомуheaders()безопасен (в отличие отcookies()— ADR-026).Почему
|ru|en— матчит любой путь, начинающийся сru/en(не ровноru/en), ломает/russia,/energyetc.dynamicParams = true(default) — unknown locale рендерится динамически с home контентом + wrong canonical./rusds12312рендерил canonical/ru(чужой URL).Watch out
headers()в rootnot-found.tsx— делает root 404 dynamic. Это безопасно: root not-found НЕ участвует в ISR/SSG страниц (рендерится только при 404 вне locale segment). ADR-026 (cookies() ломает ISR) — НЕ применяется (cookies не используются, и not-found не в ISR path)..nextcache — после удаления%5F%5F404%5F%5Froute,tsc --noEmitпадал на stale.next/types/.../%5F%5F404%5F%5F/page.ts(Cannot find module). Лечитсяrm -rf .next. CI чистый билд не имеет этой проблемы./__404__route исчез из build map (подтверждено)./[locale]помечен● (SSG)с prerender только/ru,/en./RU(uppercase) — вне scope (отдельная issue). Matcher case-sensitive,/RUНЕ исключается → middleware редиректит →/ru/RU→ 404 (черезdynamicParams=false). Актуально для контракта, но не в спеке.Pending
—
Closes #29
Code Review Summary
Качественный SEO-fix PR: исправлены 5 багов из issue #29 (matcher граница locale-сегмента, 308 permanent redirects,
dynamicParams=false, удаление soft-404 route, canonical guard). CI green, тесты обновлены совместно с изменениями (paired update), dangling references на удалённый%5F%5F404%5F%5Froute иLEGACY_REDIRECTSотсутствуют.notFound()вgenerateMetadata— валидный паттерн (подтверждено Next.js 15.1.8 docs: Metadata component ловитisHTTPAccessFallbackError→getNotFoundMetadata).Positives
(?!(?:ru|en)(?:/|$))корректно отличает/ru(locale) от/russia(non-locale). 17 regex-кейсов в тестах, все pass.LEGACY_REDIRECTSдействительно дублировалnext.config.tsredirects (next.config запускается before middleware → middleware-блок был unreachable). Обоснованно удалён.notFound()вpage.tsxзащищает canonical от регрессии, даже еслиdynamicParams=falseуже делает кейс недостижимым.%5F%5F404%5F%5F/page.tsx,LEGACY_REDIRECTS) не оставило dangling readers (rgпуст)./rusds12312,/enfoo,/russia,/energy, trailing slash).Suggestions (info, not blocking)
src/app/not-found.tsx:11-16 [duplication]
detectLocaleдублирует логикуgetLocaleизsrc/middleware.ts:5-14(обе:startsWith("en")→ "en",startsWith("ru")→ "ru", fallbackDEFAULT_LOCALE). Можно вынести вsrc/lib/i18n.tsкакlocaleFromAcceptLanguage(accept: string | null): Locale— оба consumer'а импортируют одну функцию. Не blocking: контексты разные (edge vs RSC), но логика идентична.src/app/[locale]/layout.tsx:27,65 [consistency] Layout использует
isLocale(locale) ? locale : DEFAULT_LOCALEfallback, аpage.tsx:14,37—if (!isLocale(locale)) notFound(). СdynamicParams=falsefallback в layout недостижим, но паттерны несогласованы. Info: defense-in-depth допустим, но если хочется консистентности — можно заменить fallback наnotFound()в layout тоже (осторожно:notFound()в layout имеет другую семантику — рендерит rootnot-found.tsx, не[locale]/not-found.tsx).src/app/not-found.tsx [testing] Новая
detectLocaleлогика (Accept-Language → locale) не покрыта тестами. Существующийsrc/__tests__/not-found.test.tsxтестирует толькоNotFoundViewcomponent, не rootnot-found.tsxpage. Warning: стоит добавить тест наdetectLocale(ru-RU → "ru", en-US → "en", null → DEFAULT_LOCALE).Verdict: APPROVE