Files
hamedandClaude Fable 5 2fbd24b4aa chore: remove unused NEXT_PUBLIC_CLIENT_ID/SECRET env vars
The OAuth flow uses grant_type=mobile with a server-issued grant
(app/api/auth/token/route.js); no client_id/client_secret is sent at
runtime. These vars were dead — removed from Dockerfile args/env,
docker-compose, nixpacks, and docs. Also silences the Coolify
'variable not set' build warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 11:09:19 +03:30

5.4 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Commands

npm run dev      # Dev server at http://yazd-nobat.localhost:3000 (sets HOST via cross-env)
npm run build    # Production build
npm run start    # Start production server
npm run lint     # ESLint

The dev script forces HOST=yazd-nobat.localhost so multi-domain detection works locally. To test a different city subdomain, temporarily change HOST in the script.

Environment Variables

NEXT_PUBLIC_API_URL=https://api.clinic-pro.ir   # Backend API base URL
DEV_MODE=TRUE                                    # TRUE → blocks all crawlers + noindex

DEV_MODE=TRUE disables robots indexing and adds noindex metadata. Set to FALSE in production.

Architecture Overview

Next.js 15 App Router with MUI v5 + Tailwind CSS, RTL (Persian/Farsi), Jalali calendar.

Multi-Domain System (Core Concept)

Each city has its own domain (e.g. yazd-nobat.ir, tabriz-nobat.ir). The codebase serves all cities from one deployment, detecting which city to show via subdomain.

  • data/city.json — source of truth: every city's domain, title, description, keywords, site_name, socialMedia, etc.
  • lib/getStateInfo.js — server-side: reads host header, extracts subdomain, matches against city.json. Use in Server Components and generateMetadata.
  • context/ProvinceProvider.js — client-side equivalent using window.location.hostname. Exposes useProvince(){ isProvinceInclude }.
  • lib/getCanonicalUrl.js — reads x-pathname header set by middleware for canonical URL generation.
  • middleware.js — injects x-pathname header into every response so getCanonicalUrl can read it.

Page Metadata Pattern

Every page must export generateMetadata. Layout-level metadata is the fallback:

// app/layout.js — sets title/description/OG/Twitter from matchedCity
export async function generateMetadata() {
  const { matchedCity } = await getStateInfo();
  // ...
}

Page-level generateMetadata overrides layout for dynamic pages (doctor, blog, clinic):

export async function generateMetadata({ params }) {
  const { slug } = await params; // Always await params in Next.js 15
  // fetch data, build title/description, return metadata object
}

Data Fetching

Server Components use:

  • lib/req.jsfetchReq(url) — axios with SSL verification disabled (needed for dev backend)
  • axios directly for cases needing more control

Client Components use:

  • services/api.js — axios instance with baseURL = NEXT_PUBLIC_API_URL
  • services/response.jsrequest.* — all API call wrappers. Pass { requireAuth: true } to attach the access_token cookie as Authorization: Bearer.

Authentication & Authorization

Auth uses JWT stored in cookies: access_token, refresh_token, uuid, userInfo.

  • lib/auth.jsgetUser() — reads cookies server-side
  • lib/ability.jsdefineAbilitiesFor(user) — CASL rules. Roles: "representation" → Panel access
  • Protected pages call getUser() + defineAbilitiesFor() and redirect if unauthorized

Login flow: POST /api/v1/user/send-code (OTP) → POST oauth/token → set cookies.

Styling

  • Tailwind CSS with darkMode: "class" — public pages use data-theme attribute, panel uses class
  • MUI theme configured in mui/index.js with RTL direction and Vazir font
  • Font: Vazir only — defined in app/globals.css via @font-face with font-display: swap. No other fonts.
  • Custom CSS classes in globals.css: .bg-banner-home, .bg-banner-footer, .padding-responsive, etc.
  • Dark mode toggled by next-themes in app/Providers.js: public = attribute="data-", panel = attribute="class"

Routing Structure

app/
  layout.js                  # Root layout: metadata, ThemeRegistry, ProvinceProvider
  page.js                    # Home → components/home/
  robots.js                  # Blocks all when DEV_MODE=TRUE
  sitemap.js                 # Fetches doctors/clinics/blogs from API at runtime
  doctor/[slug]/page.js      # generateMetadata + JSON-LD (Physician schema)
  clinic/[slug]/page.js      # generateMetadata + JSON-LD (MedicalClinic schema)
  blog/[slug]/page.js        # generateMetadata + JSON-LD (Article schema)
  doctors/page.js            # generateMetadata using matchedCity
  clinics/page.js            # generateMetadata using matchedCity
  panel/(layout)/            # Route group — requires "representation" role

All public pages wrap content in <Layout name="/path"> from components/layout/StLayout.js (header + footer). Panel pages use components/layoutPanel/.

Key Data Files

  • data/city.json — city configs including domain, SEO fields, social media links
  • data/state.json — province/state data, joined to city via province_id
  • data/specialties.json — medical specialties; items with parent field are sub-specialties shown in FrequentSearches

Doctor & Clinic Slugs

Both use uuid as the URL slug: /doctor/${doctor.uuid} and /clinic/${clinic.uuid}.

JSON-LD Structured Data

Added directly in page JSX (not via metadata API):

<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />

Present on /doctor/[slug] (Physician), /clinic/[slug] (MedicalClinic), /blog/[slug] (Article).