fix(seo): routing and 404 status codes #29
Labels
No labels
bug
performance
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
slaid098/slaid098-dev#29
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Контекст
Сайт slaid098.dev имеет 5 критичных SEO-багов в роутинге и обработке 404. Невалидные URL отдают HTTP 200 (soft 404 / indexable junk), middleware-матчер ломает все пути начинающиеся с
ru/en, редиректы temporary (307) вместо permanent (308), canonical указывает на чужой URL. Live-подтверждение:curl -sI https://slaid098.dev/rusds12312→ 200 (должен 404),curl -sI https://slaid098.dev/→ 307 (должен 308),curl -sI https://slaid098.dev/ru/__404__→ 200 (должен 404).Текущее состояние (подтверждено аудитом актуального кода):
src/middleware.ts:56— matcher"/((?!api|_next|favicon\\.ico|.*\\..*|ru|en).*)". Negative lookahead|ru|enматчит любой путь, первый сегмент которого НАЧИНАЕТСЯ сru/en(не только ровноru/en). Результат:/rusds12312,/enfoo,/russia,/energy— исключены из middleware → нет locale-редиректа → рендерится home страница[locale]/page.tsxсlocale="rusds12312". Подтверждено regex-тестом и live:/rusds12312→ 200,/enfoo→ 200,/foobar→ 307 →/ru/foobar(правильно).src/middleware.ts:32,47,52—NextResponse.redirect(new URL(...))без второго аргумента → по умолчанию 307 (Temporary). Все locale-редиректы (/→/ru,/apps→/ru/apps) temporary. SEO требует 308 (permanent) для постоянных редиректов — иначе Google не передаёт link equity и индексирует оба URL. Тестыsrc/__tests__/middleware.test.tsявно ожидают 307 (строки 63, 70, 77, 84, 93, 100, 107, 115) — их нужно обновить на 308.src/app/[locale]/layout.tsx— нетexport const dynamicParams = false. По умолчаниюdynamicParams = true→ неизвестные locale (/de,/fr,/xyz) рендерятся динамически через[locale]сегмент.isLocale()возвращает false →loc = DEFAULT_LOCALE = "ru"(строка 25) → рендерится Russian home с canonical/ru(строка 35). Бесконечное число indexable junk URL.src/app/[locale]/%5F%5F404%5F%5F/page.tsx— обычныйpage.tsx→ Next.js отдаёт HTTP 200. Это textbook soft 404.robots: noindex(строка 20) смягчает, но Google всё равно может флагать soft 404 и терять доверие к сайту. Подтверждено:curl -sI https://slaid098.dev/ru/__404__→ 200.src/app/[locale]/page.tsx:13,16—const loc = isLocale(locale) ? locale : DEFAULT_LOCALE→alternates: { canonical: \/${loc}` }. Для/rusds12312(locale="rusds12312") →loc="ru"→ canonical/ru, но URL/rusds12312. Wrong canonical. После фиксаdynamicParams=false` (пункт 3) этот кейс исчезнет (unknown locale → 404), но canonical должен использовать реальный locale сегмент, не fallback.Задача
src/middleware.ts— переписать matcher (строка 56) так, чтобы исключать ТОЛЬКО пути, первый сегмент которых ровноruилиen(с границей сегмента), а не всё что начинается сru/en. Рекомендуемый паттерн:"/((?!api|_next|favicon\\.ico|.*\\..*)(?!(?:ru|en)(?:/|$)).*)". Проверить regex-тестом:/rusds12312→ true (middleware РАБОТАЕТ → редирект),/ru→ false (исключён),/ru/apps→ false,/en/foo→ false,/foobar→ true,/apps→ true.src/middleware.ts— всеNextResponse.redirect(new URL(target, request.url))(строки 32, 47, 52) →NextResponse.redirect(new URL(target, request.url), 308). Обновитьsrc/__tests__/middleware.test.ts: всеexpect(res.status).toBe(307)→toBe(308).src/app/[locale]/layout.tsx— добавитьexport const dynamicParams = false;. После этого unknown locale (/de,/fr) → hard 404 (автоматически через static generation). Проверить:/de→ 404,/fr/foo→ 404.src/app/[locale]/%5F%5F404%5F%5F/page.tsx— переделать/__404__route так, чтобы он отдавал HTTP 404. Вариант (предпочтительный): убрать отдельный/__404__route целиком, перенести локализованный 404-контент вsrc/app/[locale]/not-found.tsx(который уже рендеритNotFoundView). ТогдаnotFound()в[slug]/page.tsx:70автоматически отдаст HTTP 404 с локализованным контентом. Обновитьsrc/app/not-found.tsx— убратьredirect(\/${DEFAULT_LOCALE}/404`)(строка 11), заменить на рендер root 404 (rootnot-found.tsxрендерит локализованный 404 на основе Accept-Language). Альтернатива: оставить/404но добавитьnotFound()вызов — НОnotFound()в page.tsx НЕ меняет HTTP статус на 404 в App Router (это документировано). Поэтому правильный путь — использоватьnot-found.tsx, а неpage.tsx. Уточни поведение в актуальной Next.js версии (проект на Next.js 15.5.20, см.package.json:25). ЕслиnotFound()в page.tsx действительно выставляет 404 статус (Next.js 15+ может) — оставь/404сnotFound(). Иначе — удаляй route. Покрой тестом:curl -sI https://slaid098.dev/ru/404` → 404 (после деплоя) или локальный тест статуса.src/app/[locale]/page.tsx— canonical должен использовать реальныйlocaleсегмент (с валидациейisLocale), не fallback наDEFAULT_LOCALE. Если!isLocale(locale)→notFound()(в сочетании сdynamicParams=falseэто уже не достижимо, но defense-in-depth). Для валидных locale canonical =/${locale}(уже так, строка 16). Главное — после фиксаdynamicParams=false(пункт 3) fallback-кейс исчезает.Удалить dead code:
LEGACY_REDIRECTSвsrc/middleware.ts(строки 5-8, 29-34, 44-49) — дублируетnext.config.tsredirects, unreachable (next.config redirects跑 BEFORE middleware). Удалить связанные тесты вmiddleware.test.ts(блок "middleware legacy redirects", строки 90-110) если они тестируют только dead code.Контракты
GET /rusds12312→ 308 →/ru/rusds12312→ 404 (hard 404, не 200)GET /enfoo→ 308 →/en/enfoo→ 404GET /foobar→ 308 →/ru/foobar→ 404GET /→ 308 →/ru(или/enпо Accept-Language)GET /apps→ 308 →/ru/appsGET /ru/nonexistent-slug→ 404 (hard, черезnotFound()в [slug]/page.tsx)GET /ru/__404__→ 404 (если route удалён) ИЛИ/ru/__404__перестаёт существовать и 404 рендерится через[locale]/not-found.tsxGET /de→ 404 (unknown locale,dynamicParams=false)GET /fr/foo→ 404/ru,/en,/ru/apps,/ru/opencode-config) — без изменений, 200Инварианты
ruиen(изsrc/lib/i18n.ts)dynamicParams = falseна[locale]— unknown locale → hard 404/ru,/ru/...,/en,/en/...(по границе сегмента), не пути начинающиеся сru/ennotFound()должен выставлять HTTP 404 статус (проверь в актуальной Next.js 15.5.20 — если нет, используйnot-found.tsxroute, неpage.tsx)/ru,/en,/ru/apps,/ru/contacts,/ru/subscriptions,/ru/{slug}) — без изменений в URLГраничные случаи
/RU(uppercase) — пока не в scope этого issue, но проверь что matcher не ломается/ru/apps/) — пока не в scopenew URL(target, request.url)наследует query)next.config.tslegacy redirects (/youtube,/take_break,/support) — БЕЗ изменений в этом issue/ru/ru/foo(double locale) — должно 404 (3 сегмента,[slug]не матчит)Влияние на связанные компоненты
src/middleware.ts— matcher + redirect status + удаление dead LEGACY_REDIRECTSsrc/app/[locale]/layout.tsx—dynamicParams = falsesrc/app/[locale]/%5F%5F404%5F%5F/page.tsx— удаление или переделка на real 404src/app/not-found.tsx— возможно обновление (если/__404__удаляется)src/app/[locale]/not-found.tsx— возможно обновление контентаsrc/app/[locale]/page.tsx— canonical фиксsrc/__tests__/middleware.test.ts— 307 → 308, удаление dead-code тестовsrc/__tests__/slug-static-params.test.ts— без изменений (generateStaticParams не трогаем)src/__tests__/not-found.test.tsx— возможно обновлениеsrc/app/sitemap.ts— БЕЗ изменений (не листит/__404__, корректно)Вне scope
/RU(отдельная issue)Критерии приемки
curl -sI https://slaid098.dev/rusds12312→ 308 (redirect на/ru/rusds12312), затем 404 (не 200)curl -sI https://slaid098.dev/enfoo→ 308, затем 404curl -sI https://slaid098.dev/foobar→ 308 →/ru/foobar→ 404curl -sI https://slaid098.dev/→ 308 (не 307) →/rucurl -sI https://slaid098.dev/apps→ 308 (не 307) →/ru/appscurl -sI https://slaid098.dev/ru/__404__→ 404 (не 200) ИЛИ route удалён → 404curl -sI https://slaid098.dev/de→ 404 (не 200 с home контентом)curl -sI https://slaid098.dev/fr/foo→ 404curl -sI https://slaid098.dev/ru→ 200 (без изменений)curl -sI https://slaid098.dev/en→ 200 (без изменений)curl -sI https://slaid098.dev/ru/apps→ 200 (без изменений)curl -sI https://slaid098.dev/ru/opencode-config→ 200 (без изменений)curl -sI https://slaid098.dev/ru/nonexistent-slug→ 404/ru=https://slaid098.dev/ru(без изменений для валидных)/rusds12312— отсутствует (страница 404, не рендерит canonical)npm run lint(biome) проходитnpm run typecheck(tsc) проходитnpm test(vitest) проходит, middleware-тесты обновлены на 308/rusds12312→ true (middleware работает),/ru→ false,/ru/apps→ false,/en/foo→ false,/foobar→ true