fix(seo): locale edge cases, manifest and structured data polish #31
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#31
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?
Контекст
Остаточные SEO/UX edge cases после issue #29 (routing/404) и issue #30 (hreflang/OG/robots). Uppercase locale
/RUbypass → redirect на 404, manifest.ts hardcodedlang: "ru"иstart_url: "/"для en-пользователей и PWA launch,[slug]используетog:type: "website"вместоarticle, sitemaplastmod=createdAt(неupdatedAt),switchLocalePathне валидирует существование counterpart,metadataBasemissing в root layout. По отдельности low-severity, но для цели «ни одного косяка» — все должны быть закрыты.Текущее состояние (подтверждено аудитом актуального кода):
/RU,/Enuppercase —src/middleware.ts:56matcher case-sensitive./RUне матчитru|enв lookahead → middleware РАБОТАЕТ на/RU→isLocale("RU")возвращает false (строки 317-319 проверяют lowercase) → redirect 308 →/ru/RU→[slug]→getShowcaseRepo("RU")→ null → 404. Должно:/RU→ redirect на/ru(case-insensitive locale detection). Live:curl -sI https://slaid098.dev/Ru→ 200 (баг — должно redirect).src/app/manifest.ts:12—lang: "ru"hardcoded. Manifest единственный на оба locale (/manifest.webmanifest). En-пользователи PWA получаютlang: "ru".src/app/manifest.ts:8—start_url: "/"→ middleware redirect →/{locale}. PWA launch всегда hit redirect. Должно:start_url: "/ru"(default locale) — или locale-aware (невозможно с single manifest, но можно убрать redirect hop).src/app/[locale]/[slug]/page.tsx:46—openGraph.type: "website". Для detail-страницы software repo —og:type: "article"семантически правильнее (enablespublished_time,author, richer social cards).src/app/sitemap.ts:37-39—lastModified = new Date(r.createdAt).DiscoveredRepo(src/lib/readme-discovery.ts:10-25) имеет толькоcreatedAt(нетupdatedAt). Sitemaplastmodникогда не меняется после создания repo → Google recrawl реже. НуженupdatedAtиз Forgejo API detail (полеupdated_at).src/lib/i18n.ts:321-331—switchLocalePathменяет locale префикс без проверки что counterpart URL существует.switchLocalePath("/ru/some-slug", "en")→/en/some-slugбез проверки.src/components/language-switcher.tsx:10использует это → internal links на потенциальные 404.src/app/layout.tsx— нетmetadataBase. Rootnot-found.tsxmetadata без metadataBase → URL resolve against Vercel default.[locale]/layout.tsx:28имеетmetadataBase, но root — нет.Задача
Uppercase locale —
src/middleware.ts— в началеmiddleware()функции (после парсинга segments, строка 24), добавь case-insensitive locale detection:const lowerFirst = first?.toLowerCase(); if (lowerFirst === "ru" || lowerFirst === "en") { if (first !== lowerFirst) { return NextResponse.redirect(new URL(\/${lowerFirst}${pathname.slice(first.length)}`, request.url), 308); } }. Это редиректит/RU/foo→/ru/foo(308). Не ломает existingisLocale()(который проверяет lowercase). Matcher regex не меняй (он исключает/ru//en` lowercase — для uppercase middleware сработает и сделает case-correction redirect). Обнови тесты.src/app/manifest.ts—lang: "ru"→ оставь"ru"(default locale, single manifest — нет способа сделать locale-aware без двух manifest файлов).start_url: "/"(строка 8) →start_url: "/ru"(избежать redirect hop для default-locale PWA). Если хочешь i18n manifest — это out of scope (отдельная фича).src/app/[locale]/[slug]/page.tsx—openGraph.type: "website"(строка 46) →"article". ДобавьopenGraph.publishedTime: repo.createdAt(если есть),authors: ["Sergey"](опционально). Article type требуетpublished_timeдля rich results.Sitemap lastmod —
src/lib/readme-discovery.ts— добавьupdatedAt: string | nullполе вDiscoveredRepoтип (строка 10-25), заполни из Forgejo API detail (полеupdated_at— проверьForgejoRepoDetailтип, строки 53-55, нужно добавитьupdated_at: string).discoverSingleRepoиbuildDiscoveredRepo— пробросьupdatedAt(изdetail.updated_at).makeErrorRepo—updatedAt: null.src/app/sitemap.ts:37-39—new Date(r.updatedAt ?? r.createdAt)вместоnew Date(r.createdAt)(fallback наcreatedAtеслиupdatedAtnull).switchLocalePath—src/lib/i18n.ts:321-331— оставь функцию sync (pure), но добавь комментарий/документацию что функция предполагает same-slug-both-locales (repo существует в обоих locale с одинаковым slug; статические роуты apps/contacts/subscriptions существуют в обоих locale всегда). Actual validation не нужна для текущей модели. Real async validation — out of scope (требует async refactor language-switcher).src/app/layout.tsx— добавьmetadataBase: new URL(baseUrl)(тот жеbaseUrlчто в[locale]/layout.tsx:13). Это нужно для rootnot-found.tsxmetadata URL resolution.Контракты
GET /RU→ 308 →/ruGET /RU/foo→ 308 →/ru/fooGET /En→ 308 →/enGET /En/foo→ 308 →/en/foo/manifest.webmanifest—start_url: "/ru"(не/)/ru/opencode-config—meta property="og:type" content="article"/ru/opencode-config—meta property="article:published_time"присутствует<lastmod>для repo URLs =updated_at(неcreated_at), с fallback наcreated_atеслиupdated_atnullnot-found.tsxmetadata resolves URLs againsthttps://slaid098.devИнварианты
/ru,/en)manifest.ts— single manifest,lang: "ru"(default),start_url: "/ru"(default, без redirect)og:type: "article"только для[slug]detail pages (home/apps/contacts = "website")lastmodfallback:updatedAt ?? createdAtswitchLocalePathостаётся sync (pure) — инвариант same-slug-both-locales задокументированГраничные случаи
/RU/apps→ 308 →/ru/apps(case correction + сохранение path)/Ru(mixed case) → 308 →/ru/EN/foo→ 308 →/en/fooupdated_at(API возвращает null) → sitemap fallback наcreatedAtog:type: "article"безpublished_time— валидно (опциональное поле), но лучше указать если естьВлияние на связанные компоненты
src/middleware.ts— case-insensitive locale detection (новый блок в начале функции)src/__tests__/middleware.test.ts— тесты uppercase localesrc/app/manifest.ts—start_urlsrc/app/[locale]/[slug]/page.tsx—og:type: "article",publishedTimesrc/lib/readme-discovery.ts—updatedAtполе вDiscoveredRepo+ForgejoRepoDetail+buildDiscoveredRepo+makeErrorReposrc/app/sitemap.ts—lastmod=updatedAt ?? createdAtsrc/lib/i18n.ts—switchLocalePathкомментарий/документацияsrc/app/layout.tsx—metadataBaseВне scope
switchLocalePathс real validation — требует refactor language-switcherКритерии приемки
curl -sI https://slaid098.dev/RU→ 308 →/rucurl -sI https://slaid098.dev/RU/apps→ 308 →/ru/appscurl -sI https://slaid098.dev/En→ 308 →/encurl -s https://slaid098.dev/manifest.webmanifest→start_url: "/ru"/ru/opencode-config—meta property="og:type" content="article"<lastmod>для/ru/opencode-config=updated_atдата (неcreated_at)npm run lint,npm run typecheck,npm testпроходят