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>
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'sdomain,title,description,keywords,site_name,socialMedia, etc.lib/getStateInfo.js— server-side: readshostheader, extracts subdomain, matches againstcity.json. Use in Server Components andgenerateMetadata.context/ProvinceProvider.js— client-side equivalent usingwindow.location.hostname. ExposesuseProvince()→{ isProvinceInclude }.lib/getCanonicalUrl.js— readsx-pathnameheader set by middleware for canonical URL generation.middleware.js— injectsx-pathnameheader into every response sogetCanonicalUrlcan 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.js→fetchReq(url)— axios with SSL verification disabled (needed for dev backend)axiosdirectly for cases needing more control
Client Components use:
services/api.js— axios instance withbaseURL = NEXT_PUBLIC_API_URLservices/response.js→request.*— all API call wrappers. Pass{ requireAuth: true }to attach theaccess_tokencookie asAuthorization: Bearer.
Authentication & Authorization
Auth uses JWT stored in cookies: access_token, refresh_token, uuid, userInfo.
lib/auth.js→getUser()— reads cookies server-sidelib/ability.js→defineAbilitiesFor(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 usedata-themeattribute, panel usesclass - MUI theme configured in
mui/index.jswith RTL direction and Vazir font - Font: Vazir only — defined in
app/globals.cssvia@font-facewithfont-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-themesinapp/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 linksdata/state.json— province/state data, joined to city viaprovince_iddata/specialties.json— medical specialties; items withparentfield 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).