SEO-friendly id-based URLs, sitemap/robots, and homepage category banners
- Replace legacy DB-slug URLs with /products/{id}/{fa-slug},
/products/category/{id}/{fa-slug}, and /blog/{id}/{fa-slug}, backed by
the new by-id API endpoints and a Persian slugify helper
- Add /sitemap.xml (index) + /sitemap-main.xml + /sitemap-products.xml
and /robots.txt pointing to it
- Register the three-banner-row slot in the static-image-slots catalog
- Replace the homepage category carousel with a 3-column linkable
ThreeBanners section wired to that slot
- Remove the now-superseded root prompt files (moved into Ai data/)
This commit is contained in:
@@ -4,6 +4,7 @@ import { Swiper, SwiperSlide } from "swiper/react";
|
||||
import Link from "next/link";
|
||||
import "swiper/css";
|
||||
import type { ApiBlog } from "@/lib/types";
|
||||
import { blogHref } from "@/lib/slug";
|
||||
import SectionHeading from "./ui/SectionHeading";
|
||||
|
||||
export default function BlogCarousel({ posts }: { posts: ApiBlog[] }) {
|
||||
@@ -27,7 +28,7 @@ export default function BlogCarousel({ posts }: { posts: ApiBlog[] }) {
|
||||
return (
|
||||
<SwiperSlide key={post.id}>
|
||||
<Link
|
||||
href={`/blog/${post.slug}`}
|
||||
href={blogHref(post)}
|
||||
className="group flex flex-col h-full bg-white border border-line rounded-lg overflow-hidden"
|
||||
>
|
||||
<img
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Swiper, SwiperSlide } from "swiper/react";
|
||||
import Link from "next/link";
|
||||
import type { CategoryNode } from "@/lib/types";
|
||||
import SectionHeading from "./ui/SectionHeading";
|
||||
|
||||
const PLACEHOLDER_IMAGES = [
|
||||
"/images/categories/girls.svg",
|
||||
"/images/categories/boys.svg",
|
||||
"/images/categories/baby.svg",
|
||||
"/images/categories/party.svg",
|
||||
"/images/categories/sets.svg",
|
||||
"/images/categories/outerwear.svg",
|
||||
];
|
||||
|
||||
export default function Categories({ categoryColumns }: { categoryColumns: CategoryNode[] }) {
|
||||
const items = categoryColumns.flatMap((col) => col.children);
|
||||
if (items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section className="pt-5 md:pt-7 pb-10 md:pb-14">
|
||||
<div className="boxed">
|
||||
<SectionHeading kicker="Categories" title="دستهبندی محصولات" />
|
||||
|
||||
<div className="bg-gold rounded-2xl md:rounded-3xl py-8 px-4 md:px-8">
|
||||
<Swiper
|
||||
spaceBetween={10}
|
||||
slidesPerView={2}
|
||||
breakpoints={{
|
||||
768: { slidesPerView: 3 },
|
||||
1024: { slidesPerView: 6, spaceBetween: 14 },
|
||||
1280: { slidesPerView: 8, spaceBetween: 14 },
|
||||
}}
|
||||
>
|
||||
{items.map((cat, i) => (
|
||||
<SwiperSlide key={cat.id}>
|
||||
<Link href={`/products?category=${cat.slug}`} className="group block text-center">
|
||||
<div className="overflow-hidden rounded-lg bg-white">
|
||||
<img
|
||||
src={PLACEHOLDER_IMAGES[i % PLACEHOLDER_IMAGES.length]}
|
||||
alt={cat.nameFa}
|
||||
className="w-full aspect-square object-cover transition-transform duration-300 group-hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
<span className="mt-2 block text-xs md:text-sm font-bold text-ink">
|
||||
{cat.nameFa}
|
||||
</span>
|
||||
</Link>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import Link from "next/link";
|
||||
import Logo from "./Logo";
|
||||
import { headerNavLinks, siteInfo } from "@/data/site";
|
||||
import { categoryHref } from "@/lib/slug";
|
||||
import type { CategoryNode } from "@/lib/types";
|
||||
import {
|
||||
InstagramIcon,
|
||||
@@ -66,7 +67,7 @@ export default function Footer({ categoryColumns }: { categoryColumns: CategoryN
|
||||
<ul className="space-y-2.5 text-sm">
|
||||
{categoryItems.map((cat) => (
|
||||
<li key={cat.id}>
|
||||
<Link href={`/products?category=${cat.slug}`} className="hover:text-gold transition-colors">
|
||||
<Link href={categoryHref(cat)} className="hover:text-gold transition-colors">
|
||||
{cat.nameFa}
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from "./icons";
|
||||
import { headerNavLinks } from "@/data/site";
|
||||
import { CUSTOMER_PORTAL_URL } from "@/lib/config";
|
||||
import { categoryHref } from "@/lib/slug";
|
||||
import type { CategoryNode } from "@/lib/types";
|
||||
|
||||
const socialLinks = [
|
||||
@@ -91,7 +92,7 @@ export default function Header({ categoryColumns }: { categoryColumns: CategoryN
|
||||
{categoryColumns.map((col) => (
|
||||
<div key={col.id} className="min-w-[180px]">
|
||||
<Link
|
||||
href={`/products?category=${col.slug}`}
|
||||
href={categoryHref(col)}
|
||||
className="block font-bold text-ink text-sm mb-2.5 hover:text-brand transition-colors"
|
||||
>
|
||||
{col.nameFa}
|
||||
@@ -100,7 +101,7 @@ export default function Header({ categoryColumns }: { categoryColumns: CategoryN
|
||||
{col.children.map((sub) => (
|
||||
<li key={sub.id}>
|
||||
<Link
|
||||
href={`/products?category=${sub.slug}`}
|
||||
href={categoryHref(sub)}
|
||||
className="text-[13px] text-body hover:text-gold-dark transition-colors"
|
||||
>
|
||||
{sub.nameFa}
|
||||
@@ -182,7 +183,7 @@ export default function Header({ categoryColumns }: { categoryColumns: CategoryN
|
||||
{categoryColumns.map((col) => (
|
||||
<div key={col.id}>
|
||||
<Link
|
||||
href={`/products?category=${col.slug}`}
|
||||
href={categoryHref(col)}
|
||||
className="block text-sm font-bold text-ink"
|
||||
onClick={() => setMobileOpen(false)}
|
||||
>
|
||||
@@ -192,7 +193,7 @@ export default function Header({ categoryColumns }: { categoryColumns: CategoryN
|
||||
{col.children.map((sub) => (
|
||||
<li key={sub.id}>
|
||||
<Link
|
||||
href={`/products?category=${sub.slug}`}
|
||||
href={categoryHref(sub)}
|
||||
className="text-sm text-body"
|
||||
onClick={() => setMobileOpen(false)}
|
||||
>
|
||||
|
||||
@@ -5,6 +5,7 @@ import Link from "next/link";
|
||||
import { getProducts } from "@/lib/api";
|
||||
import type { ApiProduct } from "@/lib/types";
|
||||
import { formatToman } from "@/lib/format";
|
||||
import { productHref } from "@/lib/slug";
|
||||
import { SearchIcon } from "./icons";
|
||||
|
||||
const MIN_QUERY_LENGTH = 3;
|
||||
@@ -92,7 +93,7 @@ export default function SearchBox({
|
||||
results.map((product) => (
|
||||
<Link
|
||||
key={product.id}
|
||||
href={`/products/${product.slug}`}
|
||||
href={productHref(product)}
|
||||
onClick={() => setOpen(false)}
|
||||
className="flex items-center gap-3 p-3 hover:bg-surface transition-colors border-b border-line last:border-0"
|
||||
>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { StaticImageSlot } from "@/lib/types";
|
||||
import { toCssAspectRatio } from "@/lib/format";
|
||||
import SectionHeading from "./ui/SectionHeading";
|
||||
|
||||
const fallbackBanners = [
|
||||
{ image: "/images/categories/girls.svg", href: "#" },
|
||||
{ image: "/images/categories/boys.svg", href: "#" },
|
||||
{ image: "/images/categories/baby.svg", href: "#" },
|
||||
];
|
||||
|
||||
export default function ThreeBanners({ slot }: { slot?: StaticImageSlot }) {
|
||||
const images = slot?.images ?? [];
|
||||
const cssAspectRatio = toCssAspectRatio(slot?.aspectRatio);
|
||||
const aspectStyle = cssAspectRatio ? { aspectRatio: cssAspectRatio } : undefined;
|
||||
|
||||
const banners =
|
||||
images.length > 0
|
||||
? images.map((img) => ({ image: img.url, href: img.linkUrl || undefined }))
|
||||
: fallbackBanners;
|
||||
|
||||
return (
|
||||
<section className="boxed py-10 md:py-14">
|
||||
<SectionHeading kicker="Categories" title="دستهبندیها" />
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-5">
|
||||
{banners.map((b, i) => {
|
||||
const Wrapper = b.href ? "a" : "div";
|
||||
return (
|
||||
<Wrapper
|
||||
key={i}
|
||||
{...(b.href ? { href: b.href } : {})}
|
||||
className="group block overflow-hidden rounded-2xl border border-line transition-all duration-300 hover:-translate-y-1.5 hover:border-brand hover:shadow-xl"
|
||||
>
|
||||
<img
|
||||
src={b.image}
|
||||
alt=""
|
||||
style={aspectStyle}
|
||||
className={`w-full object-cover transition-transform duration-500 ease-out group-hover:scale-110 ${
|
||||
aspectStyle ? "" : "aspect-square"
|
||||
}`}
|
||||
/>
|
||||
</Wrapper>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import type { ApiProduct, ProductDiscount } from "@/lib/types";
|
||||
import { formatToman } from "@/lib/format";
|
||||
import { productHref } from "@/lib/slug";
|
||||
import { CheckIcon, PlusIcon } from "../icons";
|
||||
|
||||
export default function ProductCard({
|
||||
@@ -29,7 +30,7 @@ export default function ProductCard({
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/products/${product.slug}`}
|
||||
href={productHref(product)}
|
||||
className="group flex flex-col h-full bg-white border border-line rounded-lg overflow-hidden"
|
||||
>
|
||||
<div className="relative">
|
||||
|
||||
Reference in New Issue
Block a user