Add Torob tags, Meshkee SEO routes, and updated static-image slots.
Product pages emit crawler-readable meta, public URLs use id plus Farsi slugs, and the homepage catalog matches the dashboard slot keys. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
f42c30a4b7
commit
c84147e765
@@ -0,0 +1,72 @@
|
||||
import type { Metadata } from "next";
|
||||
import { notFound, permanentRedirect } from "next/navigation";
|
||||
import { PageHero } from "@/components/PageHero";
|
||||
import { ProductTabs } from "@/components/ProductTabs";
|
||||
import { ProductView } from "@/components/ProductView";
|
||||
import { getComments, getProductById, getStoreItemByProduct } from "@/lib/api";
|
||||
import { productHref, productNameSlug, productTitle } from "@/lib/paths";
|
||||
import { SITE } from "@/lib/site";
|
||||
import { torobMetadata } from "@/lib/torob";
|
||||
|
||||
type Props = {
|
||||
params: Promise<{ id: string; slug: string }>;
|
||||
};
|
||||
|
||||
async function loadProduct(id: string) {
|
||||
return getProductById(id);
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
const { id } = await params;
|
||||
try {
|
||||
const product = await loadProduct(id);
|
||||
if (!product) return { title: SITE.nameFa };
|
||||
|
||||
let storeItem = null;
|
||||
try {
|
||||
storeItem = await getStoreItemByProduct(product.id);
|
||||
} catch {
|
||||
storeItem = null;
|
||||
}
|
||||
|
||||
const title = productTitle(product);
|
||||
return {
|
||||
title: `${title} | ${SITE.nameFa}`,
|
||||
description: product.summary?.trim() || SITE.tagline,
|
||||
alternates: { canonical: productHref(product) },
|
||||
...torobMetadata(product, storeItem),
|
||||
};
|
||||
} catch {
|
||||
return { title: SITE.nameFa };
|
||||
}
|
||||
}
|
||||
|
||||
export default async function ProductDetailsPage({ params }: Props) {
|
||||
const { id, slug } = await params;
|
||||
const product = await loadProduct(id);
|
||||
if (!product) notFound();
|
||||
|
||||
const title = productTitle(product);
|
||||
if (decodeURIComponent(slug) !== productNameSlug(title)) {
|
||||
permanentRedirect(productHref(product));
|
||||
}
|
||||
|
||||
const [storeItem, comments] = await Promise.all([
|
||||
getStoreItemByProduct(product.id),
|
||||
getComments("product", product.id),
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHero
|
||||
title={title}
|
||||
english={product.title}
|
||||
crumbs={[{ name: "محصولات", href: "/products" }, { name: title }]}
|
||||
/>
|
||||
<section className="container-site py-10 md:py-14">
|
||||
<ProductView product={product} variants={storeItem?.variants ?? []} />
|
||||
<ProductTabs productId={product.id} descriptionHtml={product.descriptionHtml} comments={comments} />
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { notFound, permanentRedirect } from "next/navigation";
|
||||
import { getCategoryById, getCategoryBySlug, getProduct, getProductById } from "@/lib/api";
|
||||
import { categoryHref, productHref } from "@/lib/paths";
|
||||
|
||||
/** Legacy `/products/{id-or-slug}` → canonical product or category URL. */
|
||||
export default async function LegacyProductOrCategoryRedirect({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
const { id } = await params;
|
||||
|
||||
const productById = await getProductById(id);
|
||||
if (productById) permanentRedirect(productHref(productById));
|
||||
|
||||
const productBySlug = await getProduct(id);
|
||||
if (productBySlug) permanentRedirect(productHref(productBySlug));
|
||||
|
||||
const categoryById = await getCategoryById(id);
|
||||
if (categoryById) permanentRedirect(categoryHref(categoryById));
|
||||
|
||||
const categoryBySlug = await getCategoryBySlug(id);
|
||||
if (categoryBySlug) permanentRedirect(categoryHref(categoryBySlug));
|
||||
|
||||
notFound();
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { PageHero } from "@/components/PageHero";
|
||||
import { Pagination } from "@/components/Pagination";
|
||||
import { ProductGrid } from "@/components/ProductGrid";
|
||||
import { ProductTabs } from "@/components/ProductTabs";
|
||||
import { ProductView } from "@/components/ProductView";
|
||||
import { getCategories, getCategoryBySlug, getComments, getProduct, getStoreItemByProduct } from "@/lib/api";
|
||||
import { listMappedProductsForCategory } from "@/lib/mappers";
|
||||
import { SITE } from "@/lib/site";
|
||||
|
||||
type Props = {
|
||||
params: Promise<{ slug: string }>;
|
||||
searchParams: Promise<{ page?: string }>;
|
||||
};
|
||||
|
||||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
try {
|
||||
const product = await getProduct(slug);
|
||||
if (product) {
|
||||
return {
|
||||
title: `${product.nameFa || product.title} | ${SITE.nameFa}`,
|
||||
description: product.summary || SITE.tagline,
|
||||
};
|
||||
}
|
||||
|
||||
const category = await getCategoryBySlug(slug);
|
||||
if (category) {
|
||||
return {
|
||||
title: `${category.nameFa || category.name} | ${SITE.nameFa}`,
|
||||
description: SITE.tagline,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// fall through
|
||||
}
|
||||
|
||||
return { title: SITE.nameFa };
|
||||
}
|
||||
|
||||
export default async function ProductOrCategoryPage({ params, searchParams }: Props) {
|
||||
const { slug } = await params;
|
||||
const product = await getProduct(slug);
|
||||
|
||||
if (product) {
|
||||
const [storeItem, comments] = await Promise.all([
|
||||
getStoreItemByProduct(product.id),
|
||||
getComments("product", product.id),
|
||||
]);
|
||||
return (
|
||||
<>
|
||||
<PageHero
|
||||
title={product.nameFa || product.title}
|
||||
english={product.title}
|
||||
crumbs={[{ name: "محصولات", href: "/products" }, { name: product.nameFa || product.title }]}
|
||||
/>
|
||||
<section className="container-site py-10 md:py-14">
|
||||
<ProductView product={product} variants={storeItem?.variants ?? []} />
|
||||
<ProductTabs productId={product.id} descriptionHtml={product.descriptionHtml} comments={comments} />
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const [category, categories] = await Promise.all([getCategoryBySlug(slug), getCategories()]);
|
||||
if (!category) notFound();
|
||||
|
||||
const { page: pageParam } = await searchParams;
|
||||
const page = Math.max(1, Number(pageParam) || 1);
|
||||
const children = categories.filter((item) => item.parentId === category.id);
|
||||
const data = await listMappedProductsForCategory(
|
||||
category.id,
|
||||
children.map((item) => item.id),
|
||||
page,
|
||||
16,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHero
|
||||
title={category.nameFa || category.name}
|
||||
english={category.name}
|
||||
crumbs={[{ name: "محصولات", href: "/products" }, { name: category.nameFa || category.name }]}
|
||||
/>
|
||||
<section className="container-site py-10 md:py-14">
|
||||
{children.length > 0 && (
|
||||
<div className="mb-8 flex flex-wrap gap-2">
|
||||
{children.map((item) => (
|
||||
<Link
|
||||
key={item.id}
|
||||
href={`/products/${item.slug}`}
|
||||
className="rounded-full border border-black/10 px-4 py-2 text-sm hover:border-brand hover:text-brand"
|
||||
>
|
||||
{item.nameFa || item.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<ProductGrid products={data.items} />
|
||||
<Pagination
|
||||
page={data.page}
|
||||
pageSize={data.pageSize}
|
||||
total={data.total}
|
||||
makeHref={(nextPage) => (nextPage > 1 ? `/products/${slug}?page=${nextPage}` : `/products/${slug}`)}
|
||||
/>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { notFound, permanentRedirect } from "next/navigation";
|
||||
import { PageHero } from "@/components/PageHero";
|
||||
import { Pagination } from "@/components/Pagination";
|
||||
import { ProductGrid } from "@/components/ProductGrid";
|
||||
import { getCategories, getCategoryById } from "@/lib/api";
|
||||
import { listMappedProductsForCategory } from "@/lib/mappers";
|
||||
import { categoryHref, categoryNameSlug, categoryTitle } from "@/lib/paths";
|
||||
import { SITE } from "@/lib/site";
|
||||
|
||||
type Props = {
|
||||
params: Promise<{ id: string; slug: string }>;
|
||||
searchParams: Promise<{ page?: string }>;
|
||||
};
|
||||
|
||||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
const { id } = await params;
|
||||
try {
|
||||
const category = await getCategoryById(id);
|
||||
if (!category) return { title: SITE.nameFa };
|
||||
const title = categoryTitle(category);
|
||||
return {
|
||||
title: `${title} | ${SITE.nameFa}`,
|
||||
description: category.description?.trim() || `خرید ${title} از فروشگاه لوازم ورزشی و کوهنوردی سالار استور`,
|
||||
alternates: { canonical: categoryHref(category) },
|
||||
};
|
||||
} catch {
|
||||
return { title: SITE.nameFa };
|
||||
}
|
||||
}
|
||||
|
||||
export default async function CategoryProductsPage({ params, searchParams }: Props) {
|
||||
const { id, slug } = await params;
|
||||
const [category, categories] = await Promise.all([getCategoryById(id), getCategories()]);
|
||||
if (!category || category.isActive === false) notFound();
|
||||
|
||||
const title = categoryTitle(category);
|
||||
const { page: pageParam } = await searchParams;
|
||||
const page = Math.max(1, Number(pageParam) || 1);
|
||||
|
||||
if (decodeURIComponent(slug) !== categoryNameSlug(title)) {
|
||||
const href = categoryHref(category);
|
||||
permanentRedirect(page > 1 ? `${href}?page=${page}` : href);
|
||||
}
|
||||
|
||||
const children = categories.filter((item) => item.parentId === category.id);
|
||||
const data = await listMappedProductsForCategory(
|
||||
category.id,
|
||||
children.map((item) => item.id),
|
||||
page,
|
||||
16,
|
||||
);
|
||||
const baseHref = categoryHref(category);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHero
|
||||
title={title}
|
||||
english={category.name}
|
||||
crumbs={[{ name: "محصولات", href: "/products" }, { name: title }]}
|
||||
/>
|
||||
<section className="container-site py-10 md:py-14">
|
||||
{children.length > 0 && (
|
||||
<div className="mb-8 flex flex-wrap gap-2">
|
||||
{children.map((item) => (
|
||||
<Link
|
||||
key={item.id}
|
||||
href={categoryHref(item)}
|
||||
className="rounded-full border border-black/10 px-4 py-2 text-sm hover:border-brand hover:text-brand"
|
||||
>
|
||||
{item.nameFa || item.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<ProductGrid products={data.items} />
|
||||
<Pagination
|
||||
page={data.page}
|
||||
pageSize={data.pageSize}
|
||||
total={data.total}
|
||||
makeHref={(nextPage) => (nextPage > 1 ? `${baseHref}?page=${nextPage}` : baseHref)}
|
||||
/>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -7,7 +7,8 @@ import { SITE } from "@/lib/site";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `محصولات | ${SITE.nameFa}`,
|
||||
description: SITE.tagline,
|
||||
description: "خرید لوازم ورزشی و کوهنوردی از فروشگاه سالار استور",
|
||||
alternates: { canonical: "/products" },
|
||||
};
|
||||
|
||||
type Props = {
|
||||
|
||||
Reference in New Issue
Block a user