diff --git a/Ai-data/prompt-seo.txt b/Ai-data/prompt-seo.txt
index 9177cc1..58cb317 100644
--- a/Ai-data/prompt-seo.txt
+++ b/Ai-data/prompt-seo.txt
@@ -2,11 +2,15 @@
- Do NOT ship public/sitemap.xml or public/robots.txt (nginx proxies both to Meshkee API).
- Publish GET /meshkee/sitemap-config.json generated at build time from app routes.
- Include public static routes automatically; exclude auth/checkout/cart/admin paths.
-- Omit templates unless this site uses non-default detail URLs.
+- Canonical detail URLs (Meshkee defaults — omit templates unless customized):
+ - Product: /products/{id}/{nameFaSlug} → GET /tenants/{domain}/products/by-id/{id}
+ - Blog: /blog/{id}/{titleSlug} → GET /tenants/{domain}/blogs/by-id/{id}
+ - Portfolio: /portfolios/{id}/{titleFaSlug} → GET /tenants/{domain}/portfolios/by-id/{id}
+- Slugify: Farsi letters kept, spaces → -, strip other punctuation. Do not use legacy DB slug in public URLs.
- After deploy: business dashboard → Website → Settings → Sync sitemap config (optional for CMS URLs; needed for static pages).
### On-page SEO (every public page)
- Unique
and meta description on every crawlable page
- Exactly one H1; H2/H3 for real sections
- All content images must have meaningful alt (CMS title when available)
-- Prefer CMS fields for titles/descriptions; never leave Next defaults
\ No newline at end of file
+- Prefer CMS fields for titles/descriptions; never leave Next defaults
diff --git a/CONTEXT.md b/CONTEXT.md
index c2d9cfd..11e9af3 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -40,11 +40,10 @@ This site is **catalog-only** (no shopping cart). Login / user icon goes to the
| Path | Source |
| --- | --- |
| `/` | Homepage + static image slots + store specials + latest blogs |
-| `/products` | Store items (`GET /tenants/{domain}/store-items`) |
-| `/products?categoryId=` | Same, filtered |
-| `/products/[variantId]` | Store item variant + product description/gallery + comments |
-| `/blog` | `GET /tenants/{domain}/blogs` (falls back to `lib/data.ts` `blogPosts`) |
-| `/blog/[slug]` | Blog details + comments |
+| `/products` | Catalog / store items list |
+| `/products/[id]/[slug]` | Product detail via `GET .../products/by-id/{id}` |
+| `/blog` | Blog list |
+| `/blog/[id]/[slug]` | Blog detail via `GET .../blogs/by-id/{id}` |
| `/about` | About us |
| `/contact` | Contact us |
| `/meshkee/static-image-slots` | Slot catalog for the dashboard Refresh button |
@@ -97,7 +96,10 @@ Products list/search: prefer store-items; if empty, fall back to `GET /products`
- Do **not** ship `public/sitemap.xml` or `public/robots.txt` (nginx proxies to Meshkee API).
- Build generates `lib/sitemap-config.generated.json` (`npm run generate:sitemap-config` / `prebuild`); served at `GET /meshkee/sitemap-config.json`.
- After deploy: business dashboard → Website → Settings → Sync sitemap config.
-- Templates omitted (defaults `/products/{slug}`, `/blog/{slug}` match this site).
+- Detail URLs (Meshkee defaults — omit `templates` unless customized):
+ - Product: `/products/{id}/{nameFaSlug}` — slugify `nameFa` (Farsi kept, spaces → `-`); resolve with `products/by-id/{id}`. Never use legacy DB slug in public URLs.
+ - Blog: `/blog/{id}/{titleSlug}` — `blogs/by-id/{id}`.
+- Legacy `/products/{slug}` and `/blog/{slug}` redirect to the canonical `{id}/{slug}` form.
- Every public page has unique title/description; product/blog prefer CMS fields.
## Original business data (do not invent replacements)
diff --git a/app/api/search/route.ts b/app/api/search/route.ts
index d209387..7dd3669 100644
--- a/app/api/search/route.ts
+++ b/app/api/search/route.ts
@@ -36,7 +36,7 @@ export async function GET(request: Request) {
return Response.json({
items: (catalogData.items ?? []).map((item) => ({
- id: item.slug || item.id,
+ id: item.id,
productId: item.id,
productNameFa: item.nameFa,
productTitle: item.title,
diff --git a/app/blog/[id]/[slug]/page.tsx b/app/blog/[id]/[slug]/page.tsx
new file mode 100644
index 0000000..9f5f1a4
--- /dev/null
+++ b/app/blog/[id]/[slug]/page.tsx
@@ -0,0 +1,99 @@
+import type { Metadata } from "next";
+import Link from "next/link";
+import { notFound, redirect } from "next/navigation";
+import { Container } from "@/components/Container";
+import { PageHero } from "@/components/layout/PageHero";
+import { ProductTabs } from "@/components/product/ProductTabs";
+import { Placeholder } from "@/components/Placeholder";
+import { blogHref, blogImage, blogTitleSlug, fetchBlogById } from "@/lib/blog";
+import { fetchComments } from "@/lib/comments";
+import { getBlogPost } from "@/lib/data";
+import { formatFaDate } from "@/lib/format";
+
+export const revalidate = 60;
+
+export async function generateMetadata({
+ params,
+}: {
+ params: Promise<{ id: string; slug: string }>;
+}): Promise {
+ const { id } = await params;
+ const apiPost = await fetchBlogById(id);
+ if (apiPost) return { title: apiPost.title, description: apiPost.abstract ?? undefined };
+ const local = getBlogPost(id);
+ if (!local) return { title: "مقاله" };
+ return { title: local.title, description: local.excerpt };
+}
+
+export default async function BlogPostPage({
+ params,
+}: {
+ params: Promise<{ id: string; slug: string }>;
+}) {
+ const { id, slug } = await params;
+ const apiPost = await fetchBlogById(id);
+
+ if (apiPost) {
+ const canonical = blogTitleSlug(apiPost.title);
+ if (decodeURIComponent(slug) !== canonical) {
+ redirect(blogHref(apiPost));
+ }
+
+ const comments = await fetchComments("blog", apiPost.id);
+ return (
+ <>
+
+
+
+