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>
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.irdomain). - Note on the brief: the original prompt referenced
negindentalclinic.com. That.comdomain is a parked GoDaddy lander with no content. The live site is the.irdomain, and all content/assets/palette were taken from there. - Meshkee tenant: this domain is registered as businessId
36("Negin Dental Clinic") onapi.meshkee.com, with modulesblog,videos,financeenabled. 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-tourstill 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· Aparataparat.com/v/3zqwp· Telegramnegin_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 placeholdernoreply@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
- Appointment booking — buttons currently link to
/contact-us. Decide on a real booking flow (customer portal? a dedicated form?). - Doctors page — no dedicated route yet; "معرفی پزشکان" points to
/about-us. - Virtual tour — unrecoverable (see §1). Needs a product decision: record
a new tour, or replace
/virtual-tour+ the nav link with something else. - Fonts — using Vazirmatn as a free stand-in for the original's IRANYekan (proprietary). Swap if a licensed IRANYekan is available.
- SEO — mostly done (see §10): unique title/description per page, single
H1 with clean H1→H2→H3 nesting (incl. a couple of
visually-hiddenH2s added purely to fix skipped levels — no visual change), all images have real/intentionally-empty alt text,/meshkee/sitemap-config.jsonis published. Still open: JSON-LD (Dentistschema). Do not addapp/sitemap.ts/app/robots.ts— Meshkee's nginx proxies/sitemap.xmland/robots.txtfor 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. 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.- 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.csstokens/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 throughnext/image(remote hosts are wildcarded innext.config.mjssince the upload CDN host isn't fixed). - Numbers (phone numbers, prices, dates in Latin digits) need
dir="ltr"(see the.ltr/[dir="ltr"]rule inglobals.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.tsor shippublic/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(orgenerateMetadatafor the blog detail page, pulling from CMStitle/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, orPageHero'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()inlib/meshkee.js) so a CMS author can't accidentally create a second H1 on a detail page. - All images go through
next/imagewith realalttext, except the small service/feature-card icons, which usealt=""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.jsscansapp/forpage.jsfiles 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-defaultchangefreq/priorityby adding it toPAGE_METAin that file.- Not done: JSON-LD structured data (a
Dentist/MedicalBusinessschema on Home and/orabout-uswould 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.