diff --git a/.cursor/rules/static-image-slots.mdc b/.cursor/rules/static-image-slots.mdc index 6e70996..ed0fb86 100644 --- a/.cursor/rules/static-image-slots.mdc +++ b/.cursor/rules/static-image-slots.mdc @@ -1,6 +1,6 @@ --- description: Keep Meshkee static-image slot catalog in sync with on-page image sizes. -globs: lib/static-images.ts,app/meshkee/**,components/HeroSlider.tsx,components/Banners.tsx,components/Categories.tsx,components/AboutUs.tsx,app/page.tsx,app/aboutus/page.tsx +globs: lib/static-images.ts,app/meshkee/**,components/HeroSlider.tsx,components/Banners.tsx,components/Categories.tsx,components/Brands.tsx,components/AboutUs.tsx,app/page.tsx,app/aboutus/page.tsx alwaysApply: false --- @@ -14,4 +14,4 @@ When you change a slot’s layout, CSS aspect ratio, or displayed width: 2. Keep using `slot.aspectRatio` from the Meshkee API on the rendered frame. 3. Do not invent upload or CMS APIs. Empty/failed slots keep the local placeholder. -Keys: `slider`, `banners-below-slider`, `categories`, `four-banner-section`, `three-banner-row`, `about-us-image`. +Keys: `slider` (duplicatable), `two-banner-below-slider` (2), `categories` (duplicatable), `four-banner` (4), `two-banner-row` (2), `brands` (duplicatable), `about-us-image` (2). diff --git a/Ai-data/CONTEXT.md b/Ai-data/CONTEXT.md index b7c663a..9e75b78 100644 --- a/Ai-data/CONTEXT.md +++ b/Ai-data/CONTEXT.md @@ -25,12 +25,15 @@ Source of truth: `lib/static-images.ts` (`STATIC_IMAGE_SLOT_CATALOG`). | key | kind | itemCount | aspectRatio | where | | --- | --- | --- | --- | --- | -| `slider` | list (duplicatable) | unbounded | `21:8` | Homepage hero | -| `banners-below-slider` | list | 2 | `13:5` | Two tiles under the slider | -| `categories` | list (duplicatable) | unbounded | `1:1` | Category row | -| `four-banner-section` | list | 4 | `21:9` | 2×2 banner grid | -| `three-banner-row` | list | 3 | `12:5` | Three-up row | -| `about-us-image` | single | 1 | `8:5` | Home about block + `/aboutus` hero | +| `slider` | list (duplicatable) | unbounded | `21:8` | Homepage hero. Overlay `titleFa` / `titleEn` / `subtext` when set. | +| `two-banner-below-slider` | list | 2 | `13:5` | Two tiles under the slider. Overlay `titleFa`, `titleEn`, `subtext` like the original UI. | +| `categories` | list (duplicatable) | unbounded | `1:1` | Category row. `titleFa` + `titleEn` beside the icon. | +| `four-banner` | list | 4 | `21:9` | 2×2 banner grid. Overlay `titleFa` / `titleEn` / `subtext` when set. | +| `two-banner-row` | list | 2 | `12:5` | Two-up row after top-selling. | +| `brands` | list (duplicatable) | unbounded | `2:1` | Brand logo carousel. | +| `about-us-image` | list | 2 | `8:5` | Home about uses image 1. `/aboutus` uses image 1 as hero and image 2 as the side portrait. | + +Previous keys (`banners-below-slider`, `four-banner-section`, `three-banner-row`) are still read as aliases until the dashboard Refresh imports the catalog again. Rules: diff --git a/app/aboutus/page.tsx b/app/aboutus/page.tsx index 69ea1c6..f3ea500 100644 --- a/app/aboutus/page.tsx +++ b/app/aboutus/page.tsx @@ -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 ( <>
- {image ? ( - + {hero ? ( + ) : ( {SITE.about}

- {SITE.nameFa} + {side ? ( + + ) : ( + {SITE.nameFa} + )}
diff --git a/app/blog/[slug]/page.tsx b/app/blog/[id]/[slug]/page.tsx similarity index 75% rename from app/blog/[slug]/page.tsx rename to app/blog/[id]/[slug]/page.tsx index 62b0b73..f771bbe 100644 --- a/app/blog/[slug]/page.tsx +++ b/app/blog/[id]/[slug]/page.tsx @@ -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 { - 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 { } 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 ( diff --git a/app/blog/[id]/page.tsx b/app/blog/[id]/page.tsx new file mode 100644 index 0000000..5899696 --- /dev/null +++ b/app/blog/[id]/page.tsx @@ -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(); +} diff --git a/app/blog/page.tsx b/app/blog/page.tsx index 319b53f..4485f7a 100644 --- a/app/blog/page.tsx +++ b/app/blog/page.tsx @@ -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) {
{posts.map((post) => ( - +

{post.title}

diff --git a/app/contactus/page.tsx b/app/contactus/page.tsx index 779de81..7f3b7c7 100644 --- a/app/contactus/page.tsx +++ b/app/contactus/page.tsx @@ -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 = [ diff --git a/app/layout.tsx b/app/layout.tsx index df9a501..9bf647c 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -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 ( -
+
{children}