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

301 lines
17 KiB
Markdown

# 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
```bash
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.