From b2a0fe73bcacce499f5df5c932607b944519efbd Mon Sep 17 00:00:00 2001 From: Alireza Hassani Date: Wed, 2 Sep 2026 23:43:40 +0330 Subject: [PATCH] Add Meshkee SEO, customer auth menu, and footer polish. Implement canonical product/category/blog URLs, sitemap-config endpoint, page metadata, customer login dropdown with admin handoff, safeteb-style footer bar, backend favicon, and remove the standalone partners strip. Co-authored-by: Cursor --- package-lock.json | 114 --------- public/images/MeshkeeLogo-White.png | Bin 0 -> 33035 bytes src/app/about/page.tsx | 9 +- src/app/api/auth/handoff/route.ts | 17 ++ src/app/api/auth/me/route.ts | 10 + src/app/blog/[id]/[slug]/page.tsx | 148 ++++++++++++ src/app/blog/[slug]/page.tsx | 139 +---------- src/app/blog/page.tsx | 7 +- src/app/contact/page.tsx | 7 +- src/app/globals.css | 221 ++++++++++++++++++ src/app/layout.tsx | 40 +++- src/app/meshkee/sitemap-config.json/route.ts | 23 ++ src/app/page.tsx | 13 +- src/app/products/[id]/[slug]/page.tsx | 165 +++++++++++++ src/app/products/[slug]/page.tsx | 159 +------------ .../products/category/[id]/[slug]/page.tsx | 114 +++++++++ src/app/products/page.tsx | 41 ++-- src/components/AccountMenu.tsx | 161 +++++++++++++ src/components/CategoryShowcase.tsx | 11 +- src/components/Features.tsx | 4 +- src/components/Footer.tsx | 11 +- src/components/Header.tsx | 140 +---------- src/components/HeaderNav.tsx | 216 +++++++++++++++++ src/components/HeroSlider.tsx | 38 +-- src/components/HomeContactStrip.tsx | 43 ++++ src/components/MeshkeeCredit.tsx | 31 +++ src/components/Partners.tsx | 60 ----- src/components/PostCard.tsx | 3 +- src/components/ProductCard.tsx | 3 +- src/components/Providers.tsx | 13 ++ src/lib/config.ts | 22 ++ src/lib/meshkee-server.ts | 42 ++++ src/lib/meshkee.ts | 148 +++++++++++- src/lib/seo.ts | 27 +++ src/lib/session.tsx | 188 +++++++++++++++ src/lib/shared-auth-cookie.ts | 123 ++++++++++ src/lib/slugify.ts | 9 + src/lib/urls.ts | 42 ++++ 38 files changed, 1902 insertions(+), 660 deletions(-) create mode 100644 public/images/MeshkeeLogo-White.png create mode 100644 src/app/api/auth/handoff/route.ts create mode 100644 src/app/api/auth/me/route.ts create mode 100644 src/app/blog/[id]/[slug]/page.tsx create mode 100644 src/app/meshkee/sitemap-config.json/route.ts create mode 100644 src/app/products/[id]/[slug]/page.tsx create mode 100644 src/app/products/category/[id]/[slug]/page.tsx create mode 100644 src/components/AccountMenu.tsx create mode 100644 src/components/HeaderNav.tsx create mode 100644 src/components/HomeContactStrip.tsx create mode 100644 src/components/MeshkeeCredit.tsx delete mode 100644 src/components/Partners.tsx create mode 100644 src/components/Providers.tsx create mode 100644 src/lib/config.ts create mode 100644 src/lib/meshkee-server.ts create mode 100644 src/lib/seo.ts create mode 100644 src/lib/session.tsx create mode 100644 src/lib/shared-auth-cookie.ts create mode 100644 src/lib/slugify.ts create mode 100644 src/lib/urls.ts diff --git a/package-lock.json b/package-lock.json index b912f69..4e2f4d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -631,9 +631,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -650,9 +647,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -669,9 +663,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -688,9 +679,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -707,9 +695,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -726,9 +711,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -745,9 +727,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -764,9 +743,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -783,9 +759,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -808,9 +781,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -833,9 +803,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -858,9 +825,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -883,9 +847,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -908,9 +869,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -933,9 +891,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -958,9 +913,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1228,9 +1180,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1247,9 +1196,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1266,9 +1212,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1285,9 +1228,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1526,9 +1466,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1546,9 +1483,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1566,9 +1500,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1586,9 +1517,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2139,9 +2067,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2156,9 +2081,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2173,9 +2095,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2190,9 +2109,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2207,9 +2123,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2224,9 +2137,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2241,9 +2151,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2258,9 +2165,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2275,9 +2179,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2292,9 +2193,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4979,9 +4877,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5003,9 +4898,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5027,9 +4919,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5051,9 +4940,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ diff --git a/public/images/MeshkeeLogo-White.png b/public/images/MeshkeeLogo-White.png new file mode 100644 index 0000000000000000000000000000000000000000..a10ecd330d378ce20d66f4fb430c6f2372d841db GIT binary patch literal 33035 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 literal 0 HcmV?d00001 diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx index 6b43c1e..d3eac31 100644 --- a/src/app/about/page.tsx +++ b/src/app/about/page.tsx @@ -6,13 +6,14 @@ import PageBanner from "@/components/PageBanner"; import MissionVision from "@/components/MissionVision"; import ImportCountries from "@/components/ImportCountries"; import Features from "@/components/Features"; -import Partners from "@/components/Partners"; +import { pageMetadata } from "@/lib/seo"; -export const metadata: Metadata = { +export const metadata: Metadata = pageMetadata({ title: "درباره ما | آراد آریسمان", description: "آشنایی با شرکت آراد آریسمان؛ بیش از ۲۳ سال تجربه در تولید و عرضه مکمل‌های غذایی دام، طیور و زنبور عسل.", -}; + path: "/about", +}); export default function AboutPage() { return ( @@ -84,8 +85,6 @@ export default function AboutPage() { - -
diff --git a/src/app/api/auth/handoff/route.ts b/src/app/api/auth/handoff/route.ts new file mode 100644 index 0000000..93365f5 --- /dev/null +++ b/src/app/api/auth/handoff/route.ts @@ -0,0 +1,17 @@ +import { getBearerToken, jsonResponse, meshkeeFetch, unauthorizedResponse } from "@/lib/meshkee-server"; + +export async function POST(request: Request) { + const token = getBearerToken(request); + if (!token) return unauthorizedResponse(); + + const res = await meshkeeFetch("/auth/handoff", { + method: "POST", + token, + }); + + if (!res.ok) { + return jsonResponse({ error: "امکان باز کردن پنل مدیریت نیست." }, res.status); + } + + return jsonResponse(res.data); +} diff --git a/src/app/api/auth/me/route.ts b/src/app/api/auth/me/route.ts new file mode 100644 index 0000000..53435fb --- /dev/null +++ b/src/app/api/auth/me/route.ts @@ -0,0 +1,10 @@ +import { getBearerToken, jsonResponse, meshkeeFetch, unauthorizedResponse } from "@/lib/meshkee-server"; + +export async function GET(request: Request) { + const token = getBearerToken(request); + if (!token) return unauthorizedResponse(); + + const res = await meshkeeFetch("/auth/me", { token }); + if (!res.ok) return jsonResponse({ error: "Unauthorized" }, res.status); + return jsonResponse(res.data); +} diff --git a/src/app/blog/[id]/[slug]/page.tsx b/src/app/blog/[id]/[slug]/page.tsx new file mode 100644 index 0000000..16af46e --- /dev/null +++ b/src/app/blog/[id]/[slug]/page.tsx @@ -0,0 +1,148 @@ +import type { Metadata } from "next"; +import Image from "next/image"; +import Link from "next/link"; +import { notFound, permanentRedirect } from "next/navigation"; +import Container from "@/components/Container"; +import PageBanner from "@/components/PageBanner"; +import PostCard from "@/components/PostCard"; +import { + getPostById, + getPosts, + postContent, + postDate, + postExcerpt, + postImage, + postReadingTime, + postTitle, +} from "@/lib/meshkee"; +import { pageMetadata } from "@/lib/seo"; +import { blogPath, blogSlug } from "@/lib/urls"; + +export const revalidate = 300; +export const dynamicParams = true; + +export async function generateMetadata({ + params, +}: { + params: Promise<{ id: string; slug: string }>; +}): Promise { + const { id } = await params; + const post = await getPostById(id); + if (!post) return {}; + const title = postTitle(post); + return pageMetadata({ + title: `${title} | آراد آریسمان`, + description: postExcerpt(post, 160), + path: blogPath(post), + }); +} + +export default async function BlogDetailPage({ + params, +}: { + params: Promise<{ id: string; slug: string }>; +}) { + const { id, slug: slugParam } = await params; + const slug = decodeURIComponent(slugParam); + const post = await getPostById(id); + if (!post) notFound(); + + const canonicalSlug = blogSlug(post); + if (slug !== canonicalSlug) { + permanentRedirect(blogPath(post)); + } + + const title = postTitle(post); + const image = postImage(post); + const content = postContent(post); + + const related = post.categoryId + ? (await getPosts({ categoryId: post.categoryId, pageSize: 4 })).items + .filter((p) => p.id !== post.id) + .slice(0, 3) + : []; + + return ( + <> + + +
+ + + + + + بازگشت به مقالات + + +
+ {post.categoryNameFa && ( + + {post.categoryNameFa} + + )} + {postDate(post)} + + {postReadingTime(post)} +
+ + {image && ( +
+ {title} +
+ )} + + {content && ( +
+ )} + +
+
+

نویسنده: تیم آراد آریسمان

+

+ برای مشاوره تخصصی تغذیه دام و طیور با ما در تماس باشید. +

+
+ + تماس با ما + +
+ +
+ + {related.length > 0 && ( +
+ +

+ مقالات مرتبط +

+
+ {related.map((p) => ( + + ))} +
+
+
+ )} + + ); +} diff --git a/src/app/blog/[slug]/page.tsx b/src/app/blog/[slug]/page.tsx index c955e83..dc7de45 100644 --- a/src/app/blog/[slug]/page.tsx +++ b/src/app/blog/[slug]/page.tsx @@ -1,43 +1,9 @@ -import type { Metadata } from "next"; -import Image from "next/image"; -import Link from "next/link"; -import { notFound } from "next/navigation"; -import Container from "@/components/Container"; -import PageBanner from "@/components/PageBanner"; -import PostCard from "@/components/PostCard"; -import { - getPost, - getPosts, - postContent, - postDate, - postExcerpt, - postImage, - postReadingTime, - postTitle, -} from "@/lib/meshkee"; +import { notFound, permanentRedirect } from "next/navigation"; +import { getPost } from "@/lib/meshkee"; +import { blogPath } from "@/lib/urls"; -export const revalidate = 300; - -export async function generateStaticParams() { - const { items } = await getPosts({ page: 1, pageSize: 100 }); - return items.map((p) => ({ slug: p.slug })); -} - -export async function generateMetadata({ - params, -}: { - params: Promise<{ slug: string }>; -}): Promise { - const { slug } = await params; - const post = await getPost(slug); - if (!post) return {}; - return { - title: `${postTitle(post)} | آراد آریسمان`, - description: postExcerpt(post, 160), - }; -} - -export default async function BlogDetailPage({ +/** Legacy /blog/{slug} → canonical /blog/{id}/{titleSlug} */ +export default async function LegacyBlogRedirect({ params, }: { params: Promise<{ slug: string }>; @@ -45,98 +11,5 @@ export default async function BlogDetailPage({ const { slug } = await params; const post = await getPost(slug); if (!post) notFound(); - - const title = postTitle(post); - const image = postImage(post); - const content = postContent(post); - - const related = post.categoryId - ? (await getPosts({ categoryId: post.categoryId, pageSize: 4 })).items - .filter((p) => p.id !== post.id) - .slice(0, 3) - : []; - - return ( - <> - - -
- - - - - - بازگشت به مقالات - - -
- {post.categoryNameFa && ( - - {post.categoryNameFa} - - )} - {postDate(post)} - - {postReadingTime(post)} -
- - {image && ( -
- {title} -
- )} - - {content && ( -
- )} - -
-
-

نویسنده: تیم آراد آریسمان

-

- برای مشاوره تخصصی تغذیه دام و طیور با ما در تماس باشید. -

-
- - تماس با ما - -
- -
- - {related.length > 0 && ( -
- -

- مقالات مرتبط -

-
- {related.map((p) => ( - - ))} -
-
-
- )} - - ); + permanentRedirect(blogPath(post)); } diff --git a/src/app/blog/page.tsx b/src/app/blog/page.tsx index 7af30a8..d333d4f 100644 --- a/src/app/blog/page.tsx +++ b/src/app/blog/page.tsx @@ -1,15 +1,16 @@ -import type { Metadata } from "next"; +import { pageMetadata } from "@/lib/seo"; import Container from "@/components/Container"; import PageBanner from "@/components/PageBanner"; import PostCard from "@/components/PostCard"; import Pagination from "@/components/Pagination"; import { getPosts } from "@/lib/meshkee"; -export const metadata: Metadata = { +export const metadata = pageMetadata({ title: "مقالات | آراد آریسمان", description: "مقالات آموزشی درباره تغذیه دام، طیور و زنبور عسل از تیم آراد آریسمان.", -}; + path: "/blog", +}); const PAGE_SIZE = 6; diff --git a/src/app/contact/page.tsx b/src/app/contact/page.tsx index 7bc0e12..4c0ce5e 100644 --- a/src/app/contact/page.tsx +++ b/src/app/contact/page.tsx @@ -1,14 +1,15 @@ -import type { Metadata } from "next"; import Container from "@/components/Container"; import PageBanner from "@/components/PageBanner"; import ContactInfo from "@/components/ContactInfo"; import ContactForm from "@/components/ContactForm"; +import { pageMetadata } from "@/lib/seo"; -export const metadata: Metadata = { +export const metadata = pageMetadata({ title: "تماس با ما | آراد آریسمان", description: "راه‌های ارتباطی با شرکت آراد آریسمان؛ آدرس، تلفن تماس و فرم ارسال پیام.", -}; + path: "/contact", +}); const mapQuery = encodeURIComponent( "قم، شهرک صنعتی شکوهیه، خیابان شهید بروجردی" diff --git a/src/app/globals.css b/src/app/globals.css index 5882e12..7550ced 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -68,3 +68,224 @@ body { .rich-content strong { color: var(--secondary); } + +.footer-bottom { + position: relative; + z-index: 1; + padding: 20px 0 19px; + background-color: #1d1d1d; +} + +.footer-bottom-bar { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 4px 0; +} + +.footer-bottom-site { + color: #9c9c9c; + font-size: 12px; +} + +.meshkee-credit { + position: relative; + display: inline-flex; + align-items: center; + gap: 10px; + padding: 6px 10px; + color: #9c9c9c; + text-decoration: none; + transition: color 0.3s cubic-bezier(0.22, 1, 0.36, 1); +} + +.meshkee-credit:hover { + color: #e0e0e0; + text-decoration: none; +} + +.meshkee-credit__frame { + position: absolute; + inset: 0; + pointer-events: none; +} + +.meshkee-credit__line { + position: absolute; + background: #777; + transition: transform 0.28s cubic-bezier(0.22, 1, 0.36, 1); +} + +.meshkee-credit__line--l { + top: 0; + left: 0; + width: 1px; + height: 100%; + transform: scaleY(0); + transform-origin: top; + transition-delay: 0s; +} + +.meshkee-credit__line--b { + bottom: 0; + left: 0; + width: 100%; + height: 1px; + transform: scaleX(0); + transform-origin: left; + transition-delay: 0.14s; +} + +.meshkee-credit__line--r { + top: 0; + right: 0; + width: 1px; + height: 100%; + transform: scaleY(0); + transform-origin: bottom; + transition-delay: 0.28s; +} + +.meshkee-credit__line--t { + top: 0; + left: 0; + width: 100%; + height: 1px; + transform: scaleX(0); + transform-origin: right; + transition-delay: 0.42s; +} + +.meshkee-credit:hover .meshkee-credit__line--l, +.meshkee-credit:hover .meshkee-credit__line--r { + transform: scaleY(1); +} + +.meshkee-credit:hover .meshkee-credit__line--b, +.meshkee-credit:hover .meshkee-credit__line--t { + transform: scaleX(1); +} + +.meshkee-credit:not(:hover) .meshkee-credit__line--l { + transition-delay: 0.42s; +} + +.meshkee-credit:not(:hover) .meshkee-credit__line--b { + transition-delay: 0.28s; +} + +.meshkee-credit:not(:hover) .meshkee-credit__line--r { + transition-delay: 0.14s; +} + +.meshkee-credit:not(:hover) .meshkee-credit__line--t { + transition-delay: 0s; +} + +.meshkee-credit__logo { + position: relative; + flex-shrink: 0; + width: auto; + height: 42px; + aspect-ratio: 1 / 1; + object-fit: contain; + opacity: 0.7; + transition: opacity 0.3s cubic-bezier(0.22, 1, 0.36, 1); +} + +.meshkee-credit:hover .meshkee-credit__logo { + opacity: 1; +} + +.meshkee-credit__text { + position: relative; + display: flex; + flex-direction: column; + justify-content: center; + min-height: 42px; + line-height: 1.25; +} + +.meshkee-credit__label { + font-size: 11px; +} + +.meshkee-credit__name { + font-size: 13px; + font-weight: 600; + color: #cfcfcf; + transition: color 0.3s cubic-bezier(0.22, 1, 0.36, 1); +} + +.meshkee-credit:hover .meshkee-credit__name { + color: #fff; +} + +@media only screen and (max-width: 575px) { + .footer-bottom-bar { + flex-direction: column; + justify-content: center; + text-align: center; + } +} + +.account-menu { + display: inline-flex; + position: relative; +} + +.account-menu--login { + text-decoration: none; +} + +.account-menu__trigger { + background: transparent; + cursor: pointer; + font-family: inherit; +} + +.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-start: 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: var(--secondary); + 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(--primary) 8%, white); + color: var(--primary); +} + +.account-menu__logout { + color: #b42318; +} + +.account-menu__logout:hover { + background: #fef3f2; + color: #b42318; +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index e8fee45..82ee252 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,6 +2,9 @@ import type { Metadata } from "next"; import { Vazirmatn } from "next/font/google"; import Header from "@/components/Header"; import Footer from "@/components/Footer"; +import Providers from "@/components/Providers"; +import { getBusinessId, getWebsiteFavicon } from "@/lib/meshkee"; +import { SITE_URL } from "@/lib/seo"; import "./globals.css"; const vazirmatn = Vazirmatn({ @@ -10,13 +13,30 @@ const vazirmatn = Vazirmatn({ weight: ["300", "400", "500", "600", "700", "800"], }); -export const metadata: Metadata = { - title: "آراد آریسمان | صفحه اصلی", - description: - "شرکت آراد آریسمان، تولید و عرضه مکمل‌ها، ویتامین‌ها و افزودنی‌های خوراک دام و طیور با بیش از ۲۳ سال تجربه.", -}; +const defaultFavicon = "/favicon.ico"; + +export async function generateMetadata(): Promise { + const branding = await getWebsiteFavicon(); + const iconUrl = branding.faviconUrl || defaultFavicon; + + return { + metadataBase: new URL(SITE_URL), + title: { + default: "آراد آریسمان", + template: "%s", + }, + description: + "شرکت آراد آریسمان، تولید و عرضه مکمل‌ها، ویتامین‌ها و افزودنی‌های خوراک دام و طیور با بیش از ۲۳ سال تجربه.", + icons: { + icon: iconUrl, + shortcut: iconUrl, + }, + }; +} + +export default async function RootLayout({ children }: LayoutProps<"/">) { + const businessId = await getBusinessId(); -export default function RootLayout({ children }: LayoutProps<"/">) { return ( ) { className={`${vazirmatn.variable} h-full antialiased`} > -
-
{children}
-
+ ); +} diff --git a/src/components/MeshkeeCredit.tsx b/src/components/MeshkeeCredit.tsx new file mode 100644 index 0000000..690df14 --- /dev/null +++ b/src/components/MeshkeeCredit.tsx @@ -0,0 +1,31 @@ +import Image from "next/image"; + +export default function MeshkeeCredit() { + return ( + + + ); +} diff --git a/src/components/Partners.tsx b/src/components/Partners.tsx deleted file mode 100644 index 65618c8..0000000 --- a/src/components/Partners.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import Image from "next/image"; -import Container from "./Container"; -import { getStaticImageSlot } from "@/lib/meshkee"; - -const localPartners = [ - "/images/partner-1.png", - "/images/partner-2.png", - "/images/partner-3.png", - "/images/partner-4.png", - "/images/partner-5.png", - "/images/partner-6.png", -]; - -export default async function Partners() { - const slot = await getStaticImageSlot("brands"); - const slotImages = slot?.images ?? []; - - return ( -
- -
- {slotImages.length > 0 - ? slotImages.map((item) => { - const logo = ( - {item.titleFa - ); - return item.linkUrl ? ( - - {logo} - - ) : ( - {logo} - ); - }) - : localPartners.map((src, i) => ( - نماد اعتماد - ))} -
-
-
- ); -} diff --git a/src/components/PostCard.tsx b/src/components/PostCard.tsx index 63ad716..05112fd 100644 --- a/src/components/PostCard.tsx +++ b/src/components/PostCard.tsx @@ -8,6 +8,7 @@ import { postReadingTime, postTitle, } from "@/lib/meshkee"; +import { blogPath } from "@/lib/urls"; export default function PostCard({ post }: { post: Post }) { const title = postTitle(post); @@ -16,7 +17,7 @@ export default function PostCard({ post }: { post: Post }) { return (
diff --git a/src/components/ProductCard.tsx b/src/components/ProductCard.tsx index e1f6197..ed3b1e4 100644 --- a/src/components/ProductCard.tsx +++ b/src/components/ProductCard.tsx @@ -2,6 +2,7 @@ import Image from "next/image"; import Link from "next/link"; import type { Product } from "@/lib/meshkee"; import { productImage, productPriceLabel, stripHtml } from "@/lib/meshkee"; +import { productPath } from "@/lib/urls"; export default function ProductCard({ product }: { product: Product }) { const image = productImage(product); @@ -9,7 +10,7 @@ export default function ProductCard({ product }: { product: Product }) { return (
diff --git a/src/components/Providers.tsx b/src/components/Providers.tsx new file mode 100644 index 0000000..abe0ebe --- /dev/null +++ b/src/components/Providers.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { SessionProvider } from "@/lib/session"; + +export default function Providers({ + businessId, + children, +}: { + businessId: string; + children: React.ReactNode; +}) { + return {children}; +} diff --git a/src/lib/config.ts b/src/lib/config.ts new file mode 100644 index 0000000..b572797 --- /dev/null +++ b/src/lib/config.ts @@ -0,0 +1,22 @@ +export const websiteDomain = + process.env.NEXT_PUBLIC_WEBSITE_DOMAIN ?? + process.env.WEBSITE_DOMAIN ?? + process.env.NEXT_PUBLIC_DOMAIN ?? + "aradarisman.com"; + +export const meshkeeApiBase = ( + process.env.NEXT_PUBLIC_MESHKEE_API_BASE ?? + process.env.MESHKEE_API_BASE ?? + process.env.NEXT_PUBLIC_API_BASE_URL ?? + "https://api.meshkee.com/api/v1" +).replace(/\/$/, ""); + +export const customerPortalUrl = ( + process.env.NEXT_PUBLIC_CUSTOMER_PORTAL_URL ?? + process.env.NEXT_PUBLIC_CUSTOMER_DASHBOARD_URL ?? + `https://customer.${websiteDomain}` +).replace(/\/$/, ""); + +export const businessDashboardUrl = ( + process.env.NEXT_PUBLIC_BUSINESS_ORIGIN ?? `https://business.${websiteDomain}` +).replace(/\/$/, ""); diff --git a/src/lib/meshkee-server.ts b/src/lib/meshkee-server.ts new file mode 100644 index 0000000..97b9bce --- /dev/null +++ b/src/lib/meshkee-server.ts @@ -0,0 +1,42 @@ +import { meshkeeApiBase } from "@/lib/config"; + +export function getBearerToken(request: Request): string | null { + const auth = request.headers.get("authorization"); + if (auth?.startsWith("Bearer ")) return auth.slice(7); + return null; +} + +export async function meshkeeFetch( + path: string, + init?: RequestInit & { token?: string | null }, +): Promise<{ ok: boolean; status: number; data: unknown }> { + const headers = new Headers(init?.headers); + if (!headers.has("Content-Type") && init?.body) { + headers.set("Content-Type", "application/json"); + } + if (init?.token) { + headers.set("Authorization", `Bearer ${init.token}`); + } + + const res = await fetch(`${meshkeeApiBase}${path}`, { + ...init, + headers, + }); + + let data: unknown = null; + try { + data = await res.json(); + } catch { + data = null; + } + + return { ok: res.ok, status: res.status, data }; +} + +export function jsonResponse(data: unknown, status = 200) { + return Response.json(data, { status }); +} + +export function unauthorizedResponse() { + return jsonResponse({ error: "Unauthorized" }, 401); +} diff --git a/src/lib/meshkee.ts b/src/lib/meshkee.ts index e9299f3..681fe19 100644 --- a/src/lib/meshkee.ts +++ b/src/lib/meshkee.ts @@ -3,6 +3,52 @@ import { toFa } from "@/lib/format"; const API_BASE = "https://api.meshkee.com/api/v1"; export const TENANT_DOMAIN = "aradarisman.com"; +export type Tenant = { + id: number; + name: string; + nameFa: string; + slug: string; + domain: string; +}; + +export async function getTenant(): Promise { + try { + return await meshkeeFetch(`/tenants/${TENANT_DOMAIN}`, 3600); + } catch { + return null; + } +} + +export async function getBusinessId(): Promise { + const tenant = await getTenant(); + return tenant?.id != null ? String(tenant.id) : ""; +} + +export type WebsiteFavicon = { + faviconUrl: string | null; + logoUrl: string | null; + logoDarkUrl: string | null; + hasDedicatedFavicon: boolean; +}; + +const defaultFavicon: WebsiteFavicon = { + faviconUrl: null, + logoUrl: null, + logoDarkUrl: null, + hasDedicatedFavicon: false, +}; + +export async function getWebsiteFavicon(): Promise { + try { + return await meshkeeFetch( + `/tenants/${TENANT_DOMAIN}/website/favicon`, + 3600 + ); + } catch { + return defaultFavicon; + } +} + /** * Business-managed image overrides (hero slider, category tiles, feature * icons, about photo, brand logos). Each slot has a stable `key`; when a @@ -139,6 +185,17 @@ export async function getProduct(slug: string): Promise { return data.product as Product; } +export async function getProductById(id: string): Promise { + const res = await fetch( + `${API_BASE}/tenants/${TENANT_DOMAIN}/products/by-id/${id}`, + { next: { revalidate: 300 } } + ); + if (res.status === 404) return null; + if (!res.ok) throw new Error(`Meshkee API product id "${id}" failed: ${res.status}`); + const data = await res.json(); + return data.product as Product; +} + export function productImage(product: Product): string | null { return product.thumbnail || product.image || product.thumbnailUrl || null; } @@ -151,9 +208,72 @@ export function productPriceLabel(product: Product): string { return "تماس بگیرید"; } +export type Category = { + id: string; + parentId: string | null; + name: string; + nameFa: string; + slug: string; + sortOrder: number; + isActive: boolean; +}; + +export async function getCategories(): Promise { + try { + const data = await meshkeeFetch<{ items: Category[] }>( + `/tenants/${TENANT_DOMAIN}/categories` + ); + return data.items.filter((c) => c.isActive); + } catch { + return []; + } +} + +export async function getCategoryById(id: string): Promise { + const res = await fetch( + `${API_BASE}/tenants/${TENANT_DOMAIN}/categories/by-id/${id}`, + { next: { revalidate: 300 } } + ); + if (res.status === 404) return null; + if (!res.ok) throw new Error(`Meshkee API category id "${id}" failed: ${res.status}`); + const data = await res.json(); + return data.category as Category; +} + +export async function getTopLevelCategories(): Promise { + const items = await getCategories(); + return items + .filter((c) => c.parentId === null) + .sort( + (a, b) => + a.sortOrder - b.sortOrder || + a.nameFa.localeCompare(b.nameFa, "fa") + ); +} + +/** Includes the category itself and all nested descendants. */ +export function getCategoryDescendantIds( + categoryId: string, + categories: Category[] +): string[] { + const ids = new Set([categoryId]); + let changed = true; + while (changed) { + changed = false; + for (const c of categories) { + if (c.parentId && ids.has(c.parentId) && !ids.has(c.id)) { + ids.add(c.id); + changed = true; + } + } + } + return Array.from(ids); +} + export type CategorySummary = { categoryId: string; name: string; + nameEn: string | null; count: number; image: string | null; }; @@ -172,7 +292,7 @@ export async function getCategorySummary(limit = 8): Promise const perId = new Map< string, - { id: string; name: string; count: number; image: string | null } + { id: string; name: string; nameEn: string | null; count: number; image: string | null } >(); for (const p of items) { if (!p.categoryId || !p.categoryNameFa) continue; @@ -181,8 +301,15 @@ export async function getCategorySummary(limit = 8): Promise if (entry) { entry.count += 1; if (!entry.image && img) entry.image = img; + if (!entry.nameEn && p.categoryName) entry.nameEn = p.categoryName; } else { - perId.set(p.categoryId, { id: p.categoryId, name: p.categoryNameFa, count: 1, image: img }); + perId.set(p.categoryId, { + id: p.categoryId, + name: p.categoryNameFa, + nameEn: p.categoryName ?? null, + count: 1, + image: img, + }); } } @@ -193,6 +320,7 @@ export async function getCategorySummary(limit = 8): Promise perName.set(entry.name, { categoryId: entry.id, name: entry.name, + nameEn: entry.nameEn, count: entry.count, image: entry.image, }); @@ -200,8 +328,12 @@ export async function getCategorySummary(limit = 8): Promise } existing.count += entry.count; const currentBest = perId.get(existing.categoryId)?.count ?? 0; - if (entry.count > currentBest) existing.categoryId = entry.id; + if (entry.count > currentBest) { + existing.categoryId = entry.id; + if (entry.nameEn) existing.nameEn = entry.nameEn; + } if (!existing.image && entry.image) existing.image = entry.image; + if (!existing.nameEn && entry.nameEn) existing.nameEn = entry.nameEn; } return Array.from(perName.values()) @@ -290,6 +422,16 @@ export async function getPost(slug: string): Promise { return (data.blog ?? data.post ?? data) as Post; } +export async function getPostById(id: string): Promise { + const res = await fetch(`${API_BASE}/tenants/${TENANT_DOMAIN}/blogs/by-id/${id}`, { + next: { revalidate: 300 }, + }); + if (res.status === 404) return null; + if (!res.ok) throw new Error(`Meshkee API blog id "${id}" failed: ${res.status}`); + const data = await res.json(); + return (data.blog ?? data.post ?? data) as Post; +} + export function postTitle(post: Post): string { return post.nameFa || post.titleFa || post.title || "بدون عنوان"; } diff --git a/src/lib/seo.ts b/src/lib/seo.ts new file mode 100644 index 0000000..905c8f1 --- /dev/null +++ b/src/lib/seo.ts @@ -0,0 +1,27 @@ +import type { Metadata } from "next"; + +export const SITE_URL = + process.env.NEXT_PUBLIC_SITE_URL?.replace(/\/$/, "") ?? "https://aradarisman.com"; + +export function canonicalPath(path: string): string { + if (!path || path === "/") return SITE_URL; + return `${SITE_URL}${path.startsWith("/") ? path : `/${path}`}`; +} + +export function pageMetadata({ + title, + description, + path, +}: { + title: string; + description: string; + path: string; +}): Metadata { + return { + title, + description, + alternates: { + canonical: canonicalPath(path), + }, + }; +} diff --git a/src/lib/session.tsx b/src/lib/session.tsx new file mode 100644 index 0000000..bc953a3 --- /dev/null +++ b/src/lib/session.tsx @@ -0,0 +1,188 @@ +"use client"; + +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useState, + type ReactNode, +} from "react"; + +import { + clearSharedAuthCookies, + hydrateLocalTokensFromSharedCookies, + SHARED_ACCESS_TOKEN_KEY, + SHARED_REFRESH_TOKEN_KEY, + writeSharedAuthCookies, +} from "@/lib/shared-auth-cookie"; + +export type AuthBusinessMembership = { + id: string; + name: string; + slug: string; + isOwner: boolean; + teamRole: string | null; +}; + +export type AuthUser = { + id: string; + firstName?: string; + lastName?: string; + cellNumber?: string; + email?: string; + roles?: string[]; + isSuperAdmin?: boolean; + businesses?: AuthBusinessMembership[]; +}; + +type SessionContextValue = { + user: AuthUser | null; + isAuthenticated: boolean; + ready: boolean; + businessId: string; + isWebsiteAdmin: boolean; + logout: () => void; + getAccessToken: () => string | null; +}; + +function normalizeUser(raw: Partial & { id?: string | number }): AuthUser | null { + if (raw.id == null) return null; + return { + id: String(raw.id), + firstName: raw.firstName ?? undefined, + lastName: raw.lastName ?? undefined, + cellNumber: raw.cellNumber ?? undefined, + email: raw.email ?? undefined, + roles: Array.isArray(raw.roles) ? raw.roles : undefined, + isSuperAdmin: Boolean(raw.isSuperAdmin), + businesses: Array.isArray(raw.businesses) + ? raw.businesses.map((business) => ({ + id: String(business.id), + name: business.name, + slug: business.slug, + isOwner: Boolean(business.isOwner), + teamRole: business.teamRole ?? null, + })) + : undefined, + }; +} + +export function isWebsiteAdminForBusiness(user: AuthUser | null | undefined, businessId: string): boolean { + if (!user || !businessId) return false; + if (user.isSuperAdmin || user.roles?.includes("super_admin")) return true; + return Boolean( + user.businesses?.some( + (business) => String(business.id) === String(businessId) && (business.isOwner || Boolean(business.teamRole)), + ), + ); +} + +const SessionContext = createContext(null); + +function readToken(key: string): string | null { + try { + return localStorage.getItem(key); + } catch { + return null; + } +} + +function writeToken(key: string, value: string | null) { + try { + if (value) localStorage.setItem(key, value); + else localStorage.removeItem(key); + } catch { + // ignore + } +} + +export function SessionProvider({ + children, + businessId, +}: { + children: ReactNode; + businessId: string; +}) { + const [user, setUser] = useState(null); + const [ready, setReady] = useState(false); + + const getAccessToken = useCallback(() => { + hydrateLocalTokensFromSharedCookies(SHARED_ACCESS_TOKEN_KEY, SHARED_REFRESH_TOKEN_KEY); + return ( + readToken(SHARED_ACCESS_TOKEN_KEY) || + readToken("accessToken") || + readToken("meshkeeToken") || + readToken("token") + ); + }, []); + + const clearTokens = useCallback(() => { + writeToken(SHARED_ACCESS_TOKEN_KEY, null); + writeToken(SHARED_REFRESH_TOKEN_KEY, null); + writeToken("accessToken", null); + writeToken("meshkeeToken", null); + writeToken("token", null); + clearSharedAuthCookies(); + setUser(null); + }, []); + + useEffect(() => { + const token = getAccessToken(); + if (!token) { + setReady(true); + return; + } + + const refresh = readToken(SHARED_REFRESH_TOKEN_KEY); + writeSharedAuthCookies(token, refresh ?? undefined); + + fetch("/api/auth/me", { headers: { Authorization: `Bearer ${token}` } }) + .then(async (res) => { + if (res.status === 401) { + clearTokens(); + return; + } + if (!res.ok) return; + const data = (await res.json()) as { user?: AuthUser } & AuthUser; + const next = normalizeUser(data.user ?? data); + if (next) setUser(next); + }) + .catch(() => undefined) + .finally(() => setReady(true)); + }, [clearTokens, getAccessToken]); + + const logout = useCallback(() => { + clearTokens(); + }, [clearTokens]); + + const isWebsiteAdmin = useMemo(() => isWebsiteAdminForBusiness(user, businessId), [user, businessId]); + + const value = useMemo( + () => ({ + user, + isAuthenticated: Boolean(user), + ready, + businessId, + isWebsiteAdmin, + logout, + getAccessToken, + }), + [user, ready, businessId, isWebsiteAdmin, logout, getAccessToken], + ); + + return {children}; +} + +export function useSession() { + const ctx = useContext(SessionContext); + if (!ctx) throw new Error("useSession must be used within SessionProvider"); + return ctx; +} + +export function userDisplayName(user: AuthUser | null | undefined) { + if (!user) return "کاربر"; + const full = [user.firstName, user.lastName].filter(Boolean).join(" ").trim(); + return full || user.cellNumber || user.email || "کاربر"; +} diff --git a/src/lib/shared-auth-cookie.ts b/src/lib/shared-auth-cookie.ts new file mode 100644 index 0000000..87843c4 --- /dev/null +++ b/src/lib/shared-auth-cookie.ts @@ -0,0 +1,123 @@ +/** Shared auth cookies so storefront + customer.{domain} share one login. */ + +export const SHARED_ACCESS_TOKEN_KEY = "meshkee_customer_access_token"; +export const SHARED_REFRESH_TOKEN_KEY = "meshkee_customer_refresh_token"; + +const CHUNK_SIZE = 2800; +const MAX_CHUNKS = 24; + +function cookieDomain(): string | undefined { + if (typeof window === "undefined") return undefined; + const host = window.location.hostname.toLowerCase(); + if (host === "localhost" || host === "127.0.0.1") return undefined; + const parts = host.split(".").filter(Boolean); + if (parts.length < 2) return undefined; + return `.${parts.slice(-2).join(".")}`; +} + +function readRawCookie(name: string): string | null { + if (typeof document === "undefined") return null; + const prefix = `${name}=`; + const match = document.cookie.split("; ").find((row) => row.startsWith(prefix)); + if (!match) return null; + return match.slice(prefix.length); +} + +function writeRawCookie(name: string, value: string | null) { + if (typeof document === "undefined") return; + const domain = cookieDomain(); + if (value == null) { + let cookie = `${name}=; path=/; max-age=0; SameSite=Lax`; + if (domain) cookie += `; domain=${domain}`; + document.cookie = cookie; + return; + } + const maxAge = 60 * 60 * 24 * 30; + let cookie = `${name}=${value}; path=/; max-age=${maxAge}; SameSite=Lax`; + if (domain) cookie += `; domain=${domain}`; + document.cookie = cookie; +} + +function clearChunkedCookie(name: string) { + writeRawCookie(name, null); + const n = Number(readRawCookie(`${name}_n`) || "0"); + writeRawCookie(`${name}_n`, null); + for (let i = 0; i < Math.max(n, MAX_CHUNKS); i++) { + writeRawCookie(`${name}_${i}`, null); + } +} + +function writeCookie(name: string, value: string | null) { + clearChunkedCookie(name); + if (value == null || value === "") return; + + const encoded = encodeURIComponent(value); + if (encoded.length <= 3500) { + writeRawCookie(name, encoded); + return; + } + + const chunks: string[] = []; + for (let i = 0; i < encoded.length; i += CHUNK_SIZE) { + chunks.push(encoded.slice(i, i + CHUNK_SIZE)); + } + writeRawCookie(`${name}_n`, String(chunks.length)); + chunks.forEach((chunk, i) => writeRawCookie(`${name}_${i}`, chunk)); +} + +function readCookie(name: string): string | null { + const n = Number(readRawCookie(`${name}_n`) || "0"); + if (n > 0) { + let encoded = ""; + for (let i = 0; i < n; i++) { + const part = readRawCookie(`${name}_${i}`); + if (part == null) return null; + encoded += part; + } + try { + return decodeURIComponent(encoded); + } catch { + return null; + } + } + + const raw = readRawCookie(name); + if (raw == null) return null; + try { + return decodeURIComponent(raw); + } catch { + return null; + } +} + +export function writeSharedAuthCookies(accessToken: string, refreshToken?: string) { + writeCookie(SHARED_ACCESS_TOKEN_KEY, accessToken); + if (refreshToken) writeCookie(SHARED_REFRESH_TOKEN_KEY, refreshToken); +} + +export function clearSharedAuthCookies() { + writeCookie(SHARED_ACCESS_TOKEN_KEY, null); + writeCookie(SHARED_REFRESH_TOKEN_KEY, null); +} + +export function readSharedAccessToken(): string | null { + return readCookie(SHARED_ACCESS_TOKEN_KEY); +} + +export function readSharedRefreshToken(): string | null { + return readCookie(SHARED_REFRESH_TOKEN_KEY); +} + +export function hydrateLocalTokensFromSharedCookies( + accessKey = SHARED_ACCESS_TOKEN_KEY, + refreshKey = SHARED_REFRESH_TOKEN_KEY, +): boolean { + if (typeof window === "undefined") return false; + if (localStorage.getItem(accessKey)) return false; + const access = readSharedAccessToken(); + if (!access) return false; + localStorage.setItem(accessKey, access); + const refresh = readSharedRefreshToken(); + if (refresh) localStorage.setItem(refreshKey, refresh); + return true; +} diff --git a/src/lib/slugify.ts b/src/lib/slugify.ts new file mode 100644 index 0000000..10d8b24 --- /dev/null +++ b/src/lib/slugify.ts @@ -0,0 +1,9 @@ +/** Farsi letters kept, spaces → -, other punctuation stripped. */ +export function slugifyFa(text: string): string { + return text + .trim() + .replace(/\s+/g, "-") + .replace(/[^\p{Script=Arabic}\p{N}-]+/gu, "") + .replace(/-+/g, "-") + .replace(/^-+|-+$/g, ""); +} diff --git a/src/lib/urls.ts b/src/lib/urls.ts new file mode 100644 index 0000000..7e7de84 --- /dev/null +++ b/src/lib/urls.ts @@ -0,0 +1,42 @@ +import type { Category, Post, Product } from "@/lib/meshkee"; +import { postTitle } from "@/lib/meshkee"; +import { slugifyFa } from "@/lib/slugify"; + +export function productSlug(product: Pick): string { + return slugifyFa(product.nameFa); +} + +export function productPath(product: Pick): string { + const slug = productSlug(product); + return `/products/${product.id}/${encodeURIComponent(slug)}`; +} + +export function productUrl(product: Pick): string { + return productPath(product); +} + +export function categorySlug(category: Pick): string { + return slugifyFa(category.nameFa); +} + +export function categoryPath(category: Pick): string { + const slug = categorySlug(category); + return `/products/category/${category.id}/${encodeURIComponent(slug)}`; +} + +export function categoryUrl(category: Pick): string { + return categoryPath(category); +} + +export function blogSlug(post: Post): string { + return slugifyFa(postTitle(post)); +} + +export function blogPath(post: Pick & Post): string { + const slug = blogSlug(post); + return `/blog/${post.id}/${encodeURIComponent(slug)}`; +} + +export function blogUrl(post: Pick & Post): string { + return blogPath(post); +}