diff --git a/CONTEXT.md b/CONTEXT.md index 5a6c105..7c541b0 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -2,50 +2,85 @@ ## Project -- Persian RTL storefront clone of `sohansalehnia.com` (سوهان صالح نیا, traditional Qom sohan sweets). +- Persian RTL storefront clone of `sohansalehnia.com` (سوهان صالح نیا). - Next.js 16 App Router, React 19, TypeScript, Tailwind 4, `src/app`. - Language: `fa`, direction: `rtl`. -- Local images live in `public/images`. Fonts (Iran Yekan, Montserrat ExtraLight, Fontello/WMi icons) live in `public/fonts`. +- Local images: `public/images`. Fonts: `public/fonts`. -## Scope (this clone) +## Meshkee API -Implemented pages: +- Docs: `https://api.meshkee.com/docs/website` +- API base: `https://api.meshkee.com/api/v1` +- Tenant domain: `sohansalehnia.com` (`TENANT_DOMAIN` in `src/lib/meshkee.ts`, `src/lib/config.ts`) +- All public fetches go through `src/lib/meshkee.ts`. Authenticated calls go through `src/app/api/*` + `src/lib/meshkee-server.ts`. +- Tenant id is `32` (`Salehnia Sohan` / `salehnia-sohan`). Enabled modules: products, blog, finance. -- `/` homepage -- `/aboutus` about -- `/contactus` contact (form is client-side only; no backend submit yet) -- `/products` and `/articles` are placeholders. Products, workshops, and blog will be read from Meshkee later. +## Auth / account + +- Guest login/user icon → `https://customer.sohansalehnia.com` +- Logged-in header shows `{name} عزیز` with dropdown: پروفایل من, خروج, and مدیریت وبسایت if admin. +- Profile: `https://customer.sohansalehnia.com/profile` +- Admin dashboard: `https://business.sohansalehnia.com` via `/api/auth/handoff` +- Session: `src/lib/session.tsx` + shared cookies in `src/lib/shared-auth-cookie.ts` +- Favicon: `GET /tenants/sohansalehnia.com/website/favicon`. If `faviconUrl` is empty, fall back to `/images/logo.png`. + +## Routes + +| Path | Source | +|---|---| +| `/` | static homepage clone | +| `/aboutus` | static about | +| `/contactus` | static contact (form still local) | +| `/products` | `GET /tenants/{domain}/products` | +| `/products/[id]/[slug]` | product by id + technical-info + comments + similar | +| `/articles` | `GET /tenants/{domain}/blogs` | +| `/articles/[id]/[slug]` | blog by id + comments | + +Product detail: lightbox gallery, hover zoom, tabs (توضیحات / اطلاعات فنی / دیدگاهها). Comments require login; guests see a login box. + +## SEO + sitemap + +- Do **not** add `public/sitemap.xml` or `public/robots.txt` (nginx proxies both to Meshkee API). +- Live sitemap is an index: `/sitemap.xml` → `/sitemap-main.xml` + `/sitemap-products.xml`. +- Static pages: `GET /meshkee/sitemap-config.json` (generated at build from app routes). +- Generator: `scripts/generate-sitemap-config.mjs` → `src/lib/sitemap-config.generated.json` +- Run manually: `npm run generate:sitemap-config` +- Excludes `api`, `meshkee`, `admin`, `auth`, `login`, `checkout`, `cart`, `account` +- Blog / product details are **not** listed in sitemap-config (Meshkee XML covers CMS URLs) +- After deploy: business dashboard → Website → Settings → Sync sitemap config (needed for static pages) + +### Canonical URL shapes + +Meshkee defaults — omit `templates` unless customized. This site customizes **blog** to `/articles`: + +- Product: `/products/{id}/{nameFaSlug}` → `GET /tenants/{domain}/products/by-id/{id}` +- Category: `/products/category/{id}/{nameFaSlug}` (not built; catalog empty) +- Blog: `/articles/{id}/{titleSlug}` → `GET /tenants/{domain}/blogs/by-id/{id}` (`templates.blog` in sitemap-config) +- Portfolio: not built +- Slugify (`src/lib/slugify.ts`): Farsi letters kept, spaces → `-`, strip other punctuation. Do not use legacy DB slug in public URLs. + +### On-page SEO + +- Helper: `src/lib/seo.ts` (`pageMetadata`, unique absolute `
{postDate(post)}
+ {image && ( +- مقالات بهزودی از طریق بکاند مشکی بارگذاری میشود. -
+بهزودی مقالات جدیدی منتشر میشود.
+ )}{CONTACT_PAGE.kicker}
{CONTACT_PAGE.title}
{CONTACT_PAGE.intro}
{item.title}
{item.value} diff --git a/src/app/globals.css b/src/app/globals.css index b9ff453..2071098 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -495,3 +495,110 @@ ul { text-align: center; } } + +.account-menu { + display: inline-flex; + position: relative; +} + +.account-menu__dropdown { + background: #fff; + border: 1px solid rgba(0, 0, 0, 0.06); + border-radius: 12px; + box-shadow: 0 12px 36px rgba(0, 0, 0, 0.12); + inset-inline-end: 0; + min-width: 12rem; + padding: 8px 0; + position: absolute; + top: calc(100% + 10px); + z-index: 120; +} + +.account-menu__dropdown a, +.account-menu__dropdown button { + background: transparent; + border: 0; + color: #4b4b4b; + cursor: pointer; + display: block; + font-family: inherit; + font-size: 14px; + font-weight: 500; + padding: 10px 16px; + text-align: right; + text-decoration: none; + width: 100%; +} + +.account-menu__dropdown a:hover, +.account-menu__dropdown button:hover { + background: color-mix(in srgb, var(--wb-primary-color) 8%, white); + color: var(--wb-primary-color); +} + +.account-menu__logout { + color: #b42318; +} + +.account-menu__logout:hover { + background: #fef3f2; + color: #b42318; +} + +.product-zoom { + cursor: zoom-in; +} + +.product-zoom__img { + display: block; + transition: transform 0.2s ease; + transform-origin: var(--zoom-x, 50%) var(--zoom-y, 50%); +} + +.product-zoom:hover .product-zoom__img { + transform: scale(1.7); +} + +.lightbox { + align-items: center; + background: rgba(0, 0, 0, 0.85); + display: flex; + inset: 0; + justify-content: center; + padding: 24px; + position: fixed; + z-index: 2000; +} + +.lightbox__img { + height: auto; + max-height: 90vh; + max-width: 90vw; + object-fit: contain; + width: auto; +} + +.lightbox__close { + background: transparent; + border: 0; + color: #fff; + cursor: pointer; + font-size: 36px; + line-height: 1; + position: absolute; + left: 24px; + top: 16px; +} + +.rich-content { + line-height: 2; +} + +.rich-content p { + margin-bottom: 1rem; +} + +.rich-content img { + height: auto; + max-width: 100%; +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 8572bf4..ef4af11 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,32 +1,47 @@ import type { Metadata } from "next"; import Header from "@/components/Header"; import Footer from "@/components/Footer"; +import Providers from "@/components/Providers"; +import { getBusinessId, getWebsiteFavicon } from "@/lib/meshkee"; +import { siteUrl } from "@/lib/seo"; import { SITE } from "@/lib/site"; import "./globals.css"; -export const metadata: Metadata = { - metadataBase: new URL(SITE.url), - title: { - default: SITE.name, - template: `%s | ${SITE.name}`, - }, - description: SITE.description, - icons: { - icon: "/images/logo.png", - }, -}; +const defaultFavicon = "/images/logo.png"; -export default function RootLayout({ +export async function generateMetadata(): Promiseاین صفحه وجود ندارد یا جابهجا شده است.
+ + بازگشت به صفحه اصلی + +{product.summary}
} +{productPriceLabel(product)}
+ + تماس برای سفارش + + +- فهرست محصولات بهزودی از طریق بکاند مشکی بارگذاری میشود. -
+در حال حاضر محصولی برای نمایش وجود ندارد.
+ )}{ABOUT.body}
{comment.authorName || "کاربر"}
+{comment.text}
+هنوز دیدگاهی ثبت نشده است.
+ )} + + {!ready ? null : isAuthenticated ? ( + + ) : ( +برای ثبت دیدگاه وارد حساب کاربری شوید.
+ + ورود + + +{branch.title}
آدرس ما
diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 4d8c01b..201298f 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -4,6 +4,7 @@ import Image from "next/image"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { useEffect, useState } from "react"; +import AccountMenu from "@/components/AccountMenu"; import { NAV_ITEMS, SITE } from "@/lib/site"; export default function Header() { @@ -86,13 +87,7 @@ export default function Header() {{title}
-{excerpt}
} + ادامه مطلب +{excerpt}
} + {productPriceLabel(product)} +توضیحاتی برای این محصول ثبت نشده است.
+ ))} + + {tab === "technical" && + (technicalRows.length > 0 ? ( +| + {row.label} + | +{row.value} | +
|---|
اطلاعات فنی برای این محصول ثبت نشده است.
+ ))} + + {tab === "comments" && ( +{SOHAN_FEATURE.subtitle}