diff --git a/src/app/blog/[slug]/page.tsx b/src/app/blog/[slug]/page.tsx deleted file mode 100644 index dc7de45..0000000 --- a/src/app/blog/[slug]/page.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { notFound, permanentRedirect } from "next/navigation"; -import { getPost } from "@/lib/meshkee"; -import { blogPath } from "@/lib/urls"; - -/** Legacy /blog/{slug} → canonical /blog/{id}/{titleSlug} */ -export default async function LegacyBlogRedirect({ - params, -}: { - params: Promise<{ slug: string }>; -}) { - const { slug } = await params; - const post = await getPost(slug); - if (!post) notFound(); - permanentRedirect(blogPath(post)); -} diff --git a/src/app/products/[slug]/page.tsx b/src/app/products/[slug]/page.tsx deleted file mode 100644 index 51bbaf7..0000000 --- a/src/app/products/[slug]/page.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { notFound, permanentRedirect } from "next/navigation"; -import { getProduct } from "@/lib/meshkee"; -import { productPath } from "@/lib/urls"; - -/** Legacy /products/{slug} → canonical /products/{id}/{nameFaSlug} */ -export default async function LegacyProductRedirect({ - params, -}: { - params: Promise<{ slug: string }>; -}) { - const { slug } = await params; - const product = await getProduct(slug); - if (!product) notFound(); - permanentRedirect(productPath(product)); -} diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..1c26462 --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,69 @@ +import { NextResponse, type NextRequest } from "next/server"; +import { slugifyFa } from "@/lib/slugify"; +import { TENANT_DOMAIN } from "@/lib/meshkee"; + +const API_BASE = "https://api.meshkee.com/api/v1"; + +function canonicalProductPath(product: { id: string; nameFa: string }) { + return `/products/${product.id}/${encodeURIComponent(slugifyFa(product.nameFa))}`; +} + +function canonicalBlogPath(post: { id: string; nameFa?: string; titleFa?: string; title?: string }) { + const title = post.nameFa || post.titleFa || post.title || "post"; + return `/blog/${post.id}/${encodeURIComponent(slugifyFa(title))}`; +} + +async function redirectLegacyProduct(slug: string, request: NextRequest) { + const res = await fetch( + `${API_BASE}/tenants/${TENANT_DOMAIN}/products/${encodeURIComponent(slug)}`, + { next: { revalidate: 300 } }, + ); + if (!res.ok) return null; + const data = (await res.json()) as { product?: { id: string; nameFa: string } }; + const product = data.product; + if (!product) return null; + return NextResponse.redirect(new URL(canonicalProductPath(product), request.url), 308); +} + +async function redirectLegacyBlog(slug: string, request: NextRequest) { + const res = await fetch( + `${API_BASE}/tenants/${TENANT_DOMAIN}/blogs/${encodeURIComponent(slug)}`, + { next: { revalidate: 300 } }, + ); + if (!res.ok) return null; + const data = (await res.json()) as { + blog?: { id: string; nameFa?: string; titleFa?: string; title?: string }; + post?: { id: string; nameFa?: string; titleFa?: string; title?: string }; + }; + const post = data.blog ?? data.post; + if (!post) return null; + return NextResponse.redirect(new URL(canonicalBlogPath(post), request.url), 308); +} + +export async function middleware(request: NextRequest) { + const { pathname } = request.nextUrl; + + const productMatch = pathname.match(/^\/products\/([^/]+)$/); + if (productMatch) { + const segment = decodeURIComponent(productMatch[1]); + if (segment !== "category" && !/^\d+$/.test(segment)) { + const redirect = await redirectLegacyProduct(segment, request); + if (redirect) return redirect; + } + } + + const blogMatch = pathname.match(/^\/blog\/([^/]+)$/); + if (blogMatch) { + const segment = decodeURIComponent(blogMatch[1]); + if (!/^\d+$/.test(segment)) { + const redirect = await redirectLegacyBlog(segment, request); + if (redirect) return redirect; + } + } + + return NextResponse.next(); +} + +export const config = { + matcher: ["/products/:path*", "/blog/:path*"], +};