From 25986ed088adb922efece198b4dfc495ab80e299 Mon Sep 17 00:00:00 2001 From: manager Date: Thu, 15 Oct 2026 20:33:17 +0000 Subject: [PATCH] fix(articles): 301 junk-tailed article URLs and 404 unknown slugs Crawlers looped on article URLs with an encoded tail that grew by one encoding per hop (slug%2529, slug%252529, ...). Unknown slugs rendered as 200 pages because getCurrentArticle returns {} on a miss, so every variant became an ISR file and linked to the next one. Co-Authored-By: Claude Opus 5.5 --- middleware.ts | 15 +++++++++++++++ src/pages/articles/[page].tsx | 6 ++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/middleware.ts b/middleware.ts index 3ada51fd..93a78ef0 100644 --- a/middleware.ts +++ b/middleware.ts @@ -3,6 +3,8 @@ import { NextRequest, NextResponse } from 'next/server'; const PUBLIC_FILE = /\.(.*)$/; const supportedLocales = ['en', 'ru', 'hy']; const defaultLocale = 'en'; +const ARTICLE_PATH = /^(?:\/(?:en|ru|hy))?\/articles\/([^/]+)\/?$/; +const ARTICLE_SLUG = /^[A-Za-z0-9_-]+$/; export async function middleware(request: NextRequest) { const { pathname } = request.nextUrl; @@ -30,6 +32,19 @@ export async function middleware(request: NextRequest) { return NextResponse.redirect(new URL(`/${matchedLocale}`, request.url)); } + // Crawlers follow article URLs with a stray encoded tail (`slug%29`, + // `slug%252529`, ...) that grows by one encoding per hop. Article slugs are + // [A-Za-z0-9_-] only, so anything after that prefix is cut off with a 301. + const article = ARTICLE_PATH.exec(pathname); + if (article && !ARTICLE_SLUG.test(article[1])) { + const slug = /^[A-Za-z0-9_-]+/.exec(article[1])?.[0]; + if (slug) { + const url = request.nextUrl.clone(); + url.pathname = `/articles/${slug}`; + return NextResponse.redirect(url, 301); + } + } + return NextResponse.next(); } diff --git a/src/pages/articles/[page].tsx b/src/pages/articles/[page].tsx index 364cd807..41677654 100644 --- a/src/pages/articles/[page].tsx +++ b/src/pages/articles/[page].tsx @@ -163,8 +163,10 @@ export const getStaticProps: GetStaticProps = async ({ const articles: TArticle[] = await getArticles(locale === 'ru' ? 'ru' : 'en'); const article = page && getCurrentArticle(articles, page); - if (!article) { - return { notFound: true }; + // getCurrentArticle returns {} on a miss, so an unknown slug must be caught + // by its keys: rendering it as a 200 page let crawlers loop on junk URLs. + if (!article || !Object.keys(article).length) { + return { notFound: true, revalidate: 10 }; } const recommendedArticles = getRecommendedArticles(articles, page);