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>
301 lines
17 KiB
Markdown
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.
|