// Meshkee Website API client // Docs: https://api.meshkee.com/docs/website // OpenAPI: https://api.meshkee.com/docs/website/openapi.json // // This site's apex domain is registered on Meshkee as businessId 36 // ("Negin Dental Clinic"). Public content lives under // /tenants/{domain}/... and needs no authentication. export const API_BASE = "https://api.meshkee.com/api/v1"; export const WEBSITE_DOMAIN = process.env.NEXT_PUBLIC_WEBSITE_DOMAIN || "negindentalclinic.ir"; // Customer account portal / business admin dashboard — separate Meshkee // subdomains for this business (not implemented by this Next.js app). export const CUSTOMER_PORTAL_URL = `https://customer.${WEBSITE_DOMAIN}`; export const ADMIN_DASHBOARD_URL = `https://app.${WEBSITE_DOMAIN}`; /** * Fetch a public `/tenants/{domain}/...` endpoint. * Returns `null` on any failure instead of throwing, so pages can render * a sane fallback rather than crash when the backend has nothing yet. */ async function tenantGet(path, { searchParams, revalidate = 300 } = {}) { const url = new URL(`${API_BASE}/tenants/${WEBSITE_DOMAIN}${path}`); if (searchParams) { for (const [key, value] of Object.entries(searchParams)) { if (value !== undefined && value !== null && value !== "") { url.searchParams.set(key, value); } } } try { const res = await fetch(url.toString(), { headers: { Accept: "application/json" }, next: { revalidate }, }); if (!res.ok) return null; return await res.json(); } catch { return null; } } /** { faviconUrl, logoUrl, logoDarkUrl, hasDedicatedFavicon } */ export async function getFavicon() { return tenantGet("/website/favicon", { revalidate: 3600 }); } /** About, contacts, addresses, social — mostly unfilled for this tenant today. */ export async function getBusinessInfo() { return tenantGet("/website/business-info", { revalidate: 3600 }); } /** { items, total, page, pageSize } */ export async function getBlogs({ page = 1, pageSize = 12, tag, type, categoryId, title } = {}) { const data = await tenantGet("/blogs", { searchParams: { page, pageSize, tag, type, categoryId, title }, revalidate: 120, }); return data || { items: [], total: 0, page, pageSize }; } /** { blog } — preferred for /blog/{id}/{titleSlug} detail pages */ export async function getBlogById(blogId) { const data = await tenantGet(`/blogs/by-id/${encodeURIComponent(blogId)}`, { revalidate: 120, }); return data?.blog ?? null; } export async function getBlogBySlug(slug) { const data = await tenantGet(`/blogs/${encodeURIComponent(slug)}`, { revalidate: 120, }); return data?.blog ?? null; } /** * Submit the contact form. Client-side only (called from the browser). * Required: title, name, text. Optional: email, cellNumber. */ export async function submitContact({ title, name, text, email, cellNumber }) { const res = await fetch( `${API_BASE}/tenants/${WEBSITE_DOMAIN}/contact-submissions`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title, name, text, email, cellNumber }), } ); const data = await res.json().catch(() => ({})); if (!res.ok) { throw new Error(data?.message || "ارسال پیام با خطا مواجه شد."); } return data; } // ---------- blog field helpers ---------- // The public API doesn't publish a formal schema for blog items, so read // defensively across the field-name variants Meshkee uses elsewhere // (e.g. nameFa/titleFa-style localization) and fall back gracefully. export function blogTitle(blog) { return blog?.titleFa || blog?.title || blog?.nameFa || blog?.name || "بدون عنوان"; } export function blogExcerpt(blog) { return ( blog?.excerpt || blog?.summary || blog?.description || (typeof blog?.content === "string" ? stripHtml(blog.content).slice(0, 160) : "") ); } export function blogCover(blog) { return ( blog?.coverUrl || blog?.coverImage || blog?.imageUrl || blog?.image?.url || blog?.thumbnailUrl || null ); } export function blogDate(blog) { return blog?.publishedAt || blog?.createdAt || null; } export function blogContentHtml(blog) { const html = blog?.content || blog?.body || blog?.html || ""; // The page already renders its own

(the blog title). Demote any //

the CMS editor put in the body copy so we never end up with two // on the same page. return html.replace(/<(\/?)h1(\s|>)/gi, "<$1h2$2"); } export function stripHtml(html) { return String(html).replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim(); } export function faDate(iso) { if (!iso) return ""; try { return new Intl.DateTimeFormat("fa-IR", { year: "numeric", month: "long", day: "numeric", }).format(new Date(iso)); } catch { return ""; } } /** Build the canonical `{titleSlug}` segment from a Persian title. */ export function slugifyFa(title) { return String(title || "") .trim() .replace(/\s+/g, "-") .replace(/[^؀-ۿ0-9A-Za-z-]/g, ""); }