Files
alireza-hassaniandClaude Sonnet 5 f16b47966b Initial commit: Negin Dental Clinic website
Next.js (App Router) clone of negindentalclinic.ir, wired to the
Meshkee Website API (blog, contact form, favicon), with on-page SEO
and a scroll-reveal animation sequence on the homepage.

See context.md for full project context.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 18:28:54 +03:30

17 KiB

context.md — Negin Dental Clinic (کلینیک دندانپزشکی نگین)

Interoperability context for AI systems and platforms working on this repository. Keep this file updated as the project evolves.

1. What this project is

A Next.js (App Router) re-implementation ("basic clone") of the Negin Dental Clinic website, now wired to the real Meshkee Website API backend for this business. The clinic is a real dental practice in Qom, Iran.

  • Language: Persian (fa)
  • Direction: RTL (dir="rtl" on <html>)
  • Reference site cloned: https://www.negindentalclinic.ir (built on Meshkee's web-builder; canonical host is the .ir domain).
  • Note on the brief: the original prompt referenced negindentalclinic.com. That .com domain is a parked GoDaddy lander with no content. The live site is the .ir domain, and all content/assets/palette were taken from there.
  • Meshkee tenant: this domain is registered as businessId 36 ("Negin Dental Clinic") on api.meshkee.com, with modules blog, videos, finance enabled. See §9.
  • Virtual tour: the original /assets/virtualtour.html (old Flash-era krpano export) is gone — 404 on the live origin, and only 6 non-essential files were ever archived by the Wayback Machine (no tour.xml, no panorama tiles, no viewer engine). It is not recoverable; /virtual-tour still iframes the dead URL pending a decision (new tour, placeholder, or drop the nav item — ask before changing).

2. Tech stack

Concern Choice
Framework Next.js 15.5.25 (App Router, RSC)
UI library React 19
Styling Plain CSS + CSS Modules (no Tailwind / CSS-in-JS)
Fonts next/font/google — Vazirmatn (Persian body), Cinzel (Latin/English titles)
Images next/image (local files under public/assets, plus remote Meshkee-hosted uploads — see next.config.mjs)
Language/JS JavaScript (no TypeScript)
Path alias @/* → repo root (see jsconfig.json)
Backend Meshkee Website API (https://api.meshkee.com/api/v1) — see §9

3. Directory map

app/
  layout.js              # <html lang=fa dir=rtl>, font vars, Header + Footer, dynamic favicon
  globals.css            # design tokens + shared component classes
  page.js                # Home (+ "latest blog posts" section, hidden when empty)
  page.module.css
  about-us/page.js       # درباره ما
  contact-us/page.js     # ارتباط با ما (+ ContactForm.js — posts to the Contact API)
  services/page.js       # خدمات کلینیک
  blog/page.js           # مقالات — list, backed by GET /tenants/{domain}/blogs
  blog/[id]/[slug]/page.js  # /blog/{id}/{titleSlug} detail page (canonical Meshkee URL shape)
  virtual-tour/page.js   # تور مجازی — iframes the (dead) original virtualtour.html
  meshkee/sitemap-config.json/route.js  # static-page list for the Meshkee dashboard's sitemap importer
components/
  Header.js / .module.css   # sticky nav + mobile burger + phone + account menu (client component)
  Footer.js / .module.css   # 4-col footer + Meshkee-style bottom copyright row
  PageHero.js / .module.css # bilingual sub-header band for inner pages
lib/
  meshkee.js              # Meshkee Website API client (tenant fetches, blog helpers, contact POST)
  auth-client.js           # client-side session bridge for the customer account (see §9.3)
public/assets/
  img/…                  # banners, section photos, service icons (from the original site)
  img/Header/header.png  # primary logo (کلینیک دندانپزشکی نگین / NEGIN DENTAL CLINIC)
  files/…                # footer logo + misc images from the original site

4. Design system (tokens live in app/globals.css :root)

Palette was sampled from the live site's computed styles.

Token Value Use
--brand #3b559a primary blue — buttons, headings, table header
--brand-dark #2c4380 gradients, overlays
--cyan #29abe2 logo cyan — accents, borders
--cyan-deep #1c7ba6 darker blue for the light-blue buttons
--en-accent #22356b dark navy for English/Latin captions & titles
--text #5c6873 body copy
--muted #818695 secondary copy
--heading #333232 dark headings
--bg-tint #eff6fa pale-blue section backgrounds
--bg-grey #f7f7f7 alternate section background

Typography convention (matches the original's bilingual headings and the brief's "Cinzel for English titles"): every section shows a small uppercase Cinzel English label above a Vazirmatn Persian heading. Helper classes: .sec-head, .sec-head__en, .sec-head__fa, .en-title, plus .btn, .card.

Body copy (<p>) is justified by default (text-align: justify + text-align-last: right) sitewide. Any paragraph that must stay centered (section leads, card blurbs, empty-state text, …) needs both text-align: center and text-align-last: center set explicitly on that element/class — text-align-last alone governs a single-line paragraph, so text-align: center by itself silently loses to the sitewide default. Grep for text-align-last: center in the CSS to see every guarded spot before adding a new centered <p>.

5. Content / data

Static/hardcoded (not from the API — see §9 for what is live):

  • Clinic phones: 025-32945077, 025-32945088, 025-32945350
  • Address: قم، بلوار امین، کوچه ۲۱، شماره ۵ (روبروی صدا و سیما)
  • Social: Instagram neginclinicc · Aparat aparat.com/v/3zqwp · Telegram negin_clinic
  • Insurances (طرف قرارداد): دانا، نوین، ایران، بانک ملت، معلم، کوثر، بانک تجارت، دی، سامان
  • Doctors (weekly schedule on Home): شجری، صانعی‌پور، دیناروند، کریمی، حسینی مطلق، رسولی، صبری
    • The original site's schedule table markup was partially malformed; the day→doctor mapping here is a best-effort reconstruction. Verify with the clinic before treating it as authoritative.
  • Services (12): پروتز، خدمات تخصصی، خدمات عمومی، درمان ریشه، ارتودنسی، پریو، ترمیم تخصصی (ونیر/کامپوزیت)، کودکان، جراحی ایمپلنت، مشاوره، خدمات حضوری
  • Email: info@negindentalclinic.ir (the original site literally showed a placeholder noreply@envato.com; replaced with a sensible clinic address).

GET /tenants/{domain}/website/business-info (about/emails/phones/addresses/ social) is wired in lib/meshkee.js but not yet used — the client's dashboard has it all empty right now, and the static content above is more complete. Switch the footer/contact page over to it (with the static content as fallback) once the dashboard is filled in.

6. Known gaps / TODO

  1. Appointment booking — buttons currently link to /contact-us. Decide on a real booking flow (customer portal? a dedicated form?).
  2. Doctors page — no dedicated route yet; "معرفی پزشکان" points to /about-us.
  3. Virtual tour — unrecoverable (see §1). Needs a product decision: record a new tour, or replace /virtual-tour + the nav link with something else.
  4. Fonts — using Vazirmatn as a free stand-in for the original's IRANYekan (proprietary). Swap if a licensed IRANYekan is available.
  5. SEO — mostly done (see §10): unique title/description per page, single H1 with clean H1→H2→H3 nesting (incl. a couple of visually-hidden H2s added purely to fix skipped levels — no visual change), all images have real/intentionally-empty alt text, /meshkee/sitemap-config.json is published. Still open: JSON-LD (Dentist schema). Do not add app/sitemap.ts / app/robots.ts — Meshkee's nginx proxies /sitemap.xml and /robots.txt for the live domain (see AI_PROMPT.md rule in §9); shipping local copies would conflict with those routes once this is deployed behind the Meshkee proxy.
  6. meshkee/static-image-slots — the Meshkee AI brief also asks for a catalog-of-image-slots route. Not implemented — this site doesn't use static-image slots at all yet; add it if that changes.
  7. Business-info — see §5; not wired into the UI yet.

7. Commands

npm install
npm run dev      # http://localhost:3000
npm run build    # production build
npm run start    # serve the production build

Don't run npm run build while npm run dev is pointed at the same .next/ folder — dev and production write incompatible .next output and the dev server will crash with Cannot find module './NNN.js'. If that happens: stop both, rm -rf .next node_modules/.cache, restart dev.

8. Conventions for contributors / agents

  • Keep everything RTL-first and in Persian. Latin text only for the Cinzel accent labels and brand name.
  • Use logical CSS properties (margin-inline, inset-inline-*, padding-inline).
  • New shared styles → globals.css tokens/classes; page-specific → co-located *.module.css.
  • Prefer server components; add "use client" only for interactivity (Header menu/account, ContactForm).
  • Images: local site assets go in public/assets/…; anything fetched from the Meshkee API (blog covers, favicon, logos) is a remote URL — both go through next/image (remote hosts are wildcarded in next.config.mjs since the upload CDN host isn't fixed).
  • Numbers (phone numbers, prices, dates in Latin digits) need dir="ltr" (see the .ltr / [dir="ltr"] rule in globals.css) so they don't get bidi-reordered inside RTL text.

9. Meshkee Website API integration

Docs: hub https://api.meshkee.com/docs/website · OpenAPI https://api.meshkee.com/docs/website/openapi.json · AI brief https://api.meshkee.com/docs/website/AI_PROMPT.md. Base URL https://api.meshkee.com/api/v1. This site's apex domain (negindentalclinic.ir, configurable via NEXT_PUBLIC_WEBSITE_DOMAIN) → businessId 36. All client code goes through lib/meshkee.js — add new endpoints there rather than calling fetch ad hoc from a page.

9.1 What's live right now

Feature Endpoint Where
Favicon (fallback to local logo if none uploaded) GET /tenants/{domain}/website/favicon app/layout.js (generateMetadata)
Blog list GET /tenants/{domain}/blogs app/blog/page.js
Blog detail GET /tenants/{domain}/blogs/by-id/{id} app/blog/[id]/[slug]/page.js
Latest posts on home GET /tenants/{domain}/blogs?pageSize=3 app/page.js (hidden when 0 posts)
Contact form POST /tenants/{domain}/contact-submissions app/contact-us/ContactForm.js — shows a success/error toast and resets the form

As of this writing the tenant has 0 published blogs and an empty business-info (no logo/favicon/emails/phones/addresses uploaded in the dashboard yet) — lib/meshkee.js fetchers return null/empty and every consumer has an explicit empty state, so none of this crashes; it'll just start showing real content the moment the dashboard is filled in. The contact form was tested end-to-end against the live API (real 201 response).

9.2 Blog field names — unverified

The public API doesn't publish a formal response schema for blog items (no components.schemas in the OpenAPI doc, and there are 0 posts to inspect a real payload from). lib/meshkee.js's blogTitle() / blogExcerpt() / blogCover() / blogDate() / blogContentHtml() helpers read defensively across the likely field-name variants (titleFa/title, coverUrl/ coverImage/image.url, content/body/html, …). Once the client publishes a real post, check the actual response shape and simplify/fix these helpers — they're written to degrade gracefully, not to be the final word on field names.

9.3 Header auth state — best-effort, needs confirmation

The brief: user icon → customer.{domain} when signed out; when signed in, show "{name} عزیز" with a پروفایل من / خروج dropdown, plus مدیریت وبسایت (→ app.{domain}, the admin dashboard — matches the app.negindentalclinic.ir/auth/login link found on the original live site) for admins.

Auth on this API is Bearer JWT (POST /auth/login returns { user, accessToken, refreshToken } in the body — not a shared cookie), and this storefront has no login form of its own (by design — login happens on the separate customer portal). The public docs don't say how a session on customer.{domain} is supposed to reach this site. lib/auth-client.js implements the standard fallback for that gap: the customer portal is assumed to redirect back here with ?token=&refreshToken= in the URL, which gets captured into localStorage and used to call GET /auth/me.

This hand-back mechanism is an assumption, not a documented contract. If Meshkee's customer portal actually bridges sessions differently (shared cookie on the apex domain, postMessage, etc.), update lib/auth-client.js — every consumer of auth state goes through useMeshkeeSession() / isAdminUser(), so the header and any future account-aware UI won't need to change. Also unverified: the exact field name for "is this user an admin" on the /auth/me response (isAdminUser() checks isAdmin/role/type defensively).

9.4 Hard rules from the Meshkee AI brief (apply if/when this grows)

  • Never invent endpoints outside /tenants/{domain}/... (public) and /businesses/{businessId}/... + Bearer JWT (authenticated cart/orders/ favorites) — this site currently uses neither the cart nor any auth-write endpoints.
  • Don't add app/sitemap.ts / app/robots.ts or ship public/sitemap.xml — Meshkee's proxy serves those from the API once this site is live on a Meshkee-managed domain.
  • Don't call /torob_api/... from Next.js — it's an nginx-proxied path, not a storefront fetch.

10. SEO

Per the Meshkee SEO brief (unique title/description, one clean H1→H2→H3 outline per page, real alt text, sitemap-config publishing):

  • Every route has its own metadata (or generateMetadata for the blog detail page, pulling from CMS title/excerpt) — no page relies on the root layout's default title/description except / itself.
  • Exactly one <h1> per page (the hero title on Home/blog-detail, or PageHero's <h1> everywhere else), with no skipped levels. Two spots (Home's quick-access card row, the contact page's info-card row) needed a <h2 className="visually-hidden"> added purely to fix an H1→H3 skip — they weren't meant to have a visible heading there, so this is invisible but keeps the outline correct. Same fix applied to the blog list's card grid. Blog body HTML from the CMS has any <h1> it might contain auto-demoted to <h2> (blogContentHtml() in lib/meshkee.js) so a CMS author can't accidentally create a second H1 on a detail page.
  • All images go through next/image with real alt text, except the small service/feature-card icons, which use alt="" deliberately — they sit right next to a text label that already says the same thing, so an accessible name would just be read twice by a screen reader.
  • app/meshkee/sitemap-config.json/route.js scans app/ for page.js files at build time (export const dynamic = "force-static") and returns { baseUrl, staticPages } for the Meshkee dashboard's sitemap importer. Dynamic/CMS routes (blog/[id]/[slug]) are excluded on purpose — Meshkee generates their sitemap entries itself (/sitemap-blogs.xml). New static pages are picked up automatically; give a page a non-default changefreq/priority by adding it to PAGE_META in that file.
  • Not done: JSON-LD structured data (a Dentist/MedicalBusiness schema on Home and/or about-us would be the natural next step).

11. Scroll-reveal sequence animation (Home)

components/Reveal.js wraps a block and toggles reveal--in (via IntersectionObserver) once it's ~18% into view; CSS in globals.css (.reveal, .reveal--{up,down,left,right,scale}) does the actual fade/slide/scale-in transition. Stagger a sequence with the delay prop (ms); pick a direction with from. Used throughout app/page.js: the hero plays a staggered entrance on load (eyebrow → title → lead → buttons → art), the two split sections slide in from the side each element visually sits on, and every card grid (feature cards, services, latest blog) staggers card-by-card. Pure CSS transitions (opacity/transform only, no JS-driven animation loop), auto-disabled under prefers-reduced-motion: reduce, and a <noscript> override in app/layout.js forces everything visible if JS never runs. Reuse it on other pages the same way if they need the same treatment — it's not Home-specific.