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
+22
-11
@@ -5,27 +5,30 @@ import { SlotImage } from "@/components/SlotImage";
|
||||
import { SITE } from "@/lib/site";
|
||||
import {
|
||||
STATIC_IMAGE_KEYS,
|
||||
firstImageForSlot,
|
||||
getStaticImageSlot,
|
||||
imagesForSlot,
|
||||
slotAlt,
|
||||
} from "@/lib/static-images";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `درباره ما | ${SITE.nameFa}`,
|
||||
description: SITE.about,
|
||||
alternates: { canonical: "/aboutus" },
|
||||
};
|
||||
|
||||
export default async function AboutPage() {
|
||||
const slot = await getStaticImageSlot(STATIC_IMAGE_KEYS.aboutUsImage);
|
||||
const image = slot ? firstImageForSlot([slot], STATIC_IMAGE_KEYS.aboutUsImage) : undefined;
|
||||
const images = slot ? imagesForSlot([slot], STATIC_IMAGE_KEYS.aboutUsImage) : [];
|
||||
const hero = images[0];
|
||||
const side = images[1];
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHero title="درباره ما" english="About Us" />
|
||||
<section className="container-site py-10 md:py-14">
|
||||
<div className="relative overflow-hidden rounded-2xl min-h-[280px] md:min-h-[360px]">
|
||||
{image ? (
|
||||
<SlotImage image={image} fill alt={slotAlt(image, SITE.nameFa)} />
|
||||
{hero ? (
|
||||
<SlotImage image={hero} fill alt={slotAlt(hero, SITE.nameFa)} />
|
||||
) : (
|
||||
<Image
|
||||
src="/images/pages/about-bg.webp"
|
||||
@@ -56,13 +59,21 @@ export default async function AboutPage() {
|
||||
<p className="text-zinc-600 leading-8 text-justify">{SITE.about}</p>
|
||||
</div>
|
||||
<div className="relative mx-auto w-full max-w-xs">
|
||||
<Image
|
||||
src="/images/pages/about-side.png"
|
||||
alt={SITE.nameFa}
|
||||
width={300}
|
||||
height={603}
|
||||
className="h-auto w-full object-contain"
|
||||
/>
|
||||
{side ? (
|
||||
<SlotImage
|
||||
image={side}
|
||||
alt={slotAlt(side, SITE.nameFa)}
|
||||
imgClassName="h-auto w-full object-contain"
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
src="/images/pages/about-side.png"
|
||||
alt={SITE.nameFa}
|
||||
width={300}
|
||||
height={603}
|
||||
className="h-auto w-full object-contain"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { notFound, permanentRedirect } from "next/navigation";
|
||||
import { Media } from "@/components/Media";
|
||||
import { PageHero } from "@/components/PageHero";
|
||||
import { RichContent } from "@/components/RichContent";
|
||||
import { getBlog } from "@/lib/api";
|
||||
import { getBlogById } from "@/lib/api";
|
||||
import { formatDate } from "@/lib/format";
|
||||
import { PLACEHOLDER } from "@/lib/data";
|
||||
import { blogHref, blogTitleSlug } from "@/lib/paths";
|
||||
import { SITE } from "@/lib/site";
|
||||
|
||||
type Props = {
|
||||
params: Promise<{ slug: string }>;
|
||||
params: Promise<{ id: string; slug: string }>;
|
||||
};
|
||||
|
||||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const { id } = await params;
|
||||
try {
|
||||
const blog = await getBlog(slug);
|
||||
const blog = await getBlogById(id);
|
||||
if (!blog) return { title: SITE.nameFa };
|
||||
return {
|
||||
title: `${blog.title} | ${SITE.nameFa}`,
|
||||
description: blog.abstract || SITE.aboutShort,
|
||||
description: blog.abstract?.trim() || SITE.aboutShort,
|
||||
alternates: { canonical: blogHref(blog) },
|
||||
};
|
||||
} catch {
|
||||
return { title: SITE.nameFa };
|
||||
@@ -27,10 +29,14 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
}
|
||||
|
||||
export default async function BlogDetailPage({ params }: Props) {
|
||||
const { slug } = await params;
|
||||
const blog = await getBlog(slug);
|
||||
const { id, slug } = await params;
|
||||
const blog = await getBlogById(id);
|
||||
if (!blog) notFound();
|
||||
|
||||
if (decodeURIComponent(slug) !== blogTitleSlug(blog.title)) {
|
||||
permanentRedirect(blogHref(blog));
|
||||
}
|
||||
|
||||
const author = [blog.author?.firstName, blog.author?.lastName].filter(Boolean).join(" ");
|
||||
|
||||
return (
|
||||
@@ -0,0 +1,20 @@
|
||||
import { notFound, permanentRedirect } from "next/navigation";
|
||||
import { getBlog, getBlogById } from "@/lib/api";
|
||||
import { blogHref } from "@/lib/paths";
|
||||
|
||||
/** Legacy `/blog/{slug-or-id}` → canonical `/blog/{id}/{titleSlug}`. */
|
||||
export default async function LegacyBlogRedirect({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
const { id } = await params;
|
||||
|
||||
const byId = await getBlogById(id);
|
||||
if (byId) permanentRedirect(blogHref(byId));
|
||||
|
||||
const bySlug = await getBlog(id);
|
||||
if (bySlug) permanentRedirect(blogHref(bySlug));
|
||||
|
||||
notFound();
|
||||
}
|
||||
+5
-4
@@ -9,7 +9,8 @@ import { SITE } from "@/lib/site";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `وبلاگ | ${SITE.nameFa}`,
|
||||
description: "آخرین مقالات سالار استور",
|
||||
description: "مقالات کوهنوردی، کمپینگ و راهنمای خرید لوازم ورزشی از سالار استور",
|
||||
alternates: { canonical: "/blog" },
|
||||
};
|
||||
|
||||
type Props = {
|
||||
@@ -35,11 +36,11 @@ export default async function BlogPage({ searchParams }: Props) {
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{posts.map((post) => (
|
||||
<Link
|
||||
key={post.slug}
|
||||
href={`/blog/${post.slug}`}
|
||||
key={post.id || post.slug}
|
||||
href={post.href || `/blog/${post.slug}`}
|
||||
className="overflow-hidden rounded-2xl border border-black/5 bg-white hover:border-brand/40"
|
||||
>
|
||||
<Media src={post.image} alt="" className="aspect-[16/10]" sizes="400px" />
|
||||
<Media src={post.image} alt={post.title} className="aspect-[16/10]" sizes="400px" />
|
||||
<div className="p-4">
|
||||
<time className="text-xs text-zinc-400">{post.date}</time>
|
||||
<h2 className="mt-2 text-base font-bold leading-8 line-clamp-2">{post.title}</h2>
|
||||
|
||||
@@ -6,7 +6,8 @@ import { SITE } from "@/lib/site";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `تماس با ما | ${SITE.nameFa}`,
|
||||
description: SITE.address,
|
||||
description: `تماس با فروشگاه لوازم ورزشی سالار استور — ${SITE.address}`,
|
||||
alternates: { canonical: "/contactus" },
|
||||
};
|
||||
|
||||
const contacts = [
|
||||
|
||||
+4
-2
@@ -5,6 +5,7 @@ import { Footer } from "@/components/Footer";
|
||||
import { Header } from "@/components/Header";
|
||||
import { TopHeader } from "@/components/TopHeader";
|
||||
import { getTenant } from "@/lib/api";
|
||||
import { getMenuCategories } from "@/lib/nav";
|
||||
import { SITE } from "@/lib/site";
|
||||
import "./globals.css";
|
||||
|
||||
@@ -28,19 +29,20 @@ const carter = Carter_One({
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(`https://${SITE.domain}`),
|
||||
title: `${SITE.nameFa} | ${SITE.tagline}`,
|
||||
description: SITE.aboutShort,
|
||||
};
|
||||
|
||||
export default async function RootLayout({ children }: LayoutProps<"/">) {
|
||||
const tenant = await getTenant();
|
||||
const [tenant, menuCategories] = await Promise.all([getTenant(), getMenuCategories()]);
|
||||
|
||||
return (
|
||||
<html lang="fa" dir="rtl" className={`${vazir.variable} ${oswald.variable} ${carter.variable} h-full antialiased`}>
|
||||
<body className="min-h-full flex flex-col bg-white text-ink">
|
||||
<AppProviders businessId={tenant ? String(tenant.id) : ""}>
|
||||
<TopHeader />
|
||||
<Header />
|
||||
<Header menuCategories={menuCategories} />
|
||||
<main className="flex-1">{children}</main>
|
||||
<Footer />
|
||||
</AppProviders>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import sitemapConfig from "@/lib/sitemap-config.generated.json";
|
||||
|
||||
const headers = {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type",
|
||||
"Cache-Control": "public, max-age=60",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
};
|
||||
|
||||
export async function OPTIONS() {
|
||||
return new Response(null, { status: 204, headers });
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
return Response.json(sitemapConfig, { headers });
|
||||
}
|
||||
@@ -1,4 +1,11 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { SITE } from "@/lib/site";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `صفحه پیدا نشد | ${SITE.nameFa}`,
|
||||
description: "صفحه مورد نظر در سالار استور پیدا نشد.",
|
||||
};
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
|
||||
+28
-16
@@ -1,5 +1,5 @@
|
||||
import { AboutUs } from "@/components/AboutUs";
|
||||
import { BannerGrid4, BannerRow3 } from "@/components/Banners";
|
||||
import { BannerGrid4, BannerRow } from "@/components/Banners";
|
||||
import { BlogCarousel } from "@/components/BlogCarousel";
|
||||
import { Brands } from "@/components/Brands";
|
||||
import { Categories } from "@/components/Categories";
|
||||
@@ -8,44 +8,56 @@ import { SpecialSale } from "@/components/SpecialSale";
|
||||
import { TopSelling } from "@/components/TopSelling";
|
||||
import {
|
||||
STATIC_IMAGE_KEYS,
|
||||
firstImageForSlot,
|
||||
getStaticImageSlots,
|
||||
imagesForSlot,
|
||||
slotByKey,
|
||||
} from "@/lib/static-images";
|
||||
import { SITE } from "@/lib/site";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `${SITE.nameFa} | ${SITE.tagline}`,
|
||||
description: SITE.aboutShort,
|
||||
alternates: { canonical: "/" },
|
||||
};
|
||||
|
||||
export default async function Home() {
|
||||
const slots = await getStaticImageSlots("home");
|
||||
const sliderSlot = slotByKey(slots, STATIC_IMAGE_KEYS.slider);
|
||||
const sideSlot = slotByKey(slots, STATIC_IMAGE_KEYS.bannersBelowSlider);
|
||||
const fourSlot = slotByKey(slots, STATIC_IMAGE_KEYS.fourBannerSection);
|
||||
const threeSlot = slotByKey(slots, STATIC_IMAGE_KEYS.threeBannerRow);
|
||||
const sideSlot = slotByKey(slots, STATIC_IMAGE_KEYS.twoBannerBelowSlider);
|
||||
const categorySlot = slotByKey(slots, STATIC_IMAGE_KEYS.categories);
|
||||
const fourSlot = slotByKey(slots, STATIC_IMAGE_KEYS.fourBanner);
|
||||
const twoRowSlot = slotByKey(slots, STATIC_IMAGE_KEYS.twoBannerRow);
|
||||
const brandsSlot = slotByKey(slots, STATIC_IMAGE_KEYS.brands);
|
||||
const aboutSlot = slotByKey(slots, STATIC_IMAGE_KEYS.aboutUsImage);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="sr-only">
|
||||
{SITE.nameFa}؛ {SITE.tagline}
|
||||
</h1>
|
||||
<HeroSlider
|
||||
images={imagesForSlot(slots, STATIC_IMAGE_KEYS.slider)}
|
||||
aspectRatio={sliderSlot?.aspectRatio}
|
||||
sideImages={imagesForSlot(slots, STATIC_IMAGE_KEYS.bannersBelowSlider)}
|
||||
sideImages={imagesForSlot(slots, STATIC_IMAGE_KEYS.twoBannerBelowSlider)}
|
||||
sideAspectRatio={sideSlot?.aspectRatio}
|
||||
/>
|
||||
<SpecialSale />
|
||||
<Categories images={imagesForSlot(slots, STATIC_IMAGE_KEYS.categories)} />
|
||||
<Categories
|
||||
images={imagesForSlot(slots, STATIC_IMAGE_KEYS.categories)}
|
||||
aspectRatio={categorySlot?.aspectRatio}
|
||||
/>
|
||||
<BannerGrid4
|
||||
images={imagesForSlot(slots, STATIC_IMAGE_KEYS.fourBannerSection)}
|
||||
images={imagesForSlot(slots, STATIC_IMAGE_KEYS.fourBanner)}
|
||||
aspectRatio={fourSlot?.aspectRatio}
|
||||
/>
|
||||
<TopSelling />
|
||||
<BannerRow3
|
||||
images={imagesForSlot(slots, STATIC_IMAGE_KEYS.threeBannerRow)}
|
||||
aspectRatio={threeSlot?.aspectRatio}
|
||||
/>
|
||||
<Brands />
|
||||
<AboutUs
|
||||
image={firstImageForSlot(slots, STATIC_IMAGE_KEYS.aboutUsImage)}
|
||||
aspectRatio={aboutSlot?.aspectRatio}
|
||||
<BannerRow
|
||||
images={imagesForSlot(slots, STATIC_IMAGE_KEYS.twoBannerRow)}
|
||||
aspectRatio={twoRowSlot?.aspectRatio}
|
||||
/>
|
||||
<Brands images={imagesForSlot(slots, STATIC_IMAGE_KEYS.brands)} aspectRatio={brandsSlot?.aspectRatio} />
|
||||
<AboutUs images={imagesForSlot(slots, STATIC_IMAGE_KEYS.aboutUsImage)} aspectRatio={aboutSlot?.aspectRatio} />
|
||||
<BlogCarousel />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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