# Developer & AI Agent System Architecture Guide

This document defines system architecture, directory conventions, API contracts, and the Joomla data bridge for the **Global Assignment Help (GAH) Next.js 14+ (App Router)** codebase.

The Joomla backend behind this app is **multi-brand**: the same API and Bearer token serve several sites (GAH, IAH, GAH_AU, IAH_AU, ...), each backed by its own database. Which brand a request sees is selected entirely by a cookie — see [§4](#4-multi-brand-site-switching-the-webhost-cookie). This app is currently configured for **GAH**.

---

## 1. Tech Stack Overview

- **Framework:** Next.js 14+ (App Router, Turbopack, React Server Components)
- **Language:** TypeScript (Strict mode enabled)
- **Styling:** Tailwind CSS with custom responsive utilities and Google Fonts (`Inter` & `Outfit`). CMS article HTML (`dangerouslySetInnerHTML`) is styled via `@tailwindcss/typography`'s `prose`/`prose-green` classes, registered in `app/globals.css` with `@plugin "@tailwindcss/typography"` (Tailwind v4's CSS-first plugin syntax) — without this the plugin package must actually be installed, or those classes silently do nothing.
- **Icons:** Lucide Icons (`lucide-react`)
- **Backend API & Data Layer:**
  - Joomla Native Web Services REST API (`/api/index.php/v1/`) with Bearer Token Authorization
  - Next.js Route Handlers (`app/api/navigation/route.ts`, `app/api/home/route.ts`)
  - Centralized Service Manager (`services/joomlaService.ts`)

---

## 2. Directory & Component Architecture

```text
├── app/
│   ├── api/
│   │   ├── navigation/
│   │   │   └── route.ts             # Route Handler serving GET /api/navigation
│   │   └── home/
│   │       └── route.ts             # Route Handler serving GET /api/home
│   ├── [...slug]/
│   │   └── page.tsx                 # Dynamic Catch-All Route (returns 404 via notFound() if not in Joomla DB)
│   ├── layout.tsx                   # Root layout calling getNavigationData() & mounting <Header/> and <Footer/>
│   ├── page.tsx                     # Home page: hero calculator + Joomla home article + structured sections
│   └── globals.css                  # Tailwind CSS directives & global rules
├── components/
│   ├── common/
│   │   ├── Logo.tsx                 # Crisp SVG branding component
│   │   └── Button.tsx               # Primary, Secondary, WhatsApp & Accent buttons
│   ├── home/
│   │   └── OrderCalculator.tsx      # Interactive price estimation calculator component
│   └── layout/
│       ├── Header/
│       │   ├── Header.tsx           # Main Server Component wrapper
│       │   ├── TopBar.tsx           # Top banner, contact links, WhatsApp, region selector
│       │   ├── Navbar.tsx           # Brand logo, nav dropdowns, search trigger, cart & order CTAs
│       │   ├── NavDropdown.tsx      # Interactive client component (hover/click/keyboard & Mega Menu)
│       │   └── MobileMenu.tsx       # Responsive sliding drawer menu for mobile
│       └── Footer/
│           ├── Footer.tsx           # 5-column footer structure with trust badges & disclaimer
│           ├── FooterCol.tsx        # Dynamic link list column renderer
│           └── SocialLinks.tsx      # Social icons & app download buttons
├── services/
│   └── joomlaService.ts             # Central manager: fetches + builds nav tree/footer/article data from Joomla
├── types/
│   ├── navigation.ts                # TypeScript schemas for all menu/footer entities
│   └── home.ts                      # TypeScript schemas for home page & calculator
├── data/
│   ├── fallbackNavigation.ts        # Header/footer data used only if the Joomla API is unreachable
│   └── fallbackHomeData.ts          # Home page data used only if the Joomla API is unreachable
├── .env.local                       # Local environment variables (see §5)
├── .env.example                     # Environment configuration template
└── AGENTS.md                        # This file
```

Nothing in `app/` or `components/` hardcodes page content, nav links, or contact details — all of that is fetched live from Joomla in `services/joomlaService.ts`. `data/fallback*.ts` exists solely as a network-outage safety net (still populated with GAH's real public contact/social info, not placeholder data), never as the primary source.

---

## 3. Strict 404 Routing & Link Isolation Policy

1. **No External Redirection to Live Site:**
   - Internal menu links stay relative to the Next.js application (e.g. `/do-my-assignment`, `/about-us`, `/reviews`). `joomlaService` builds every Joomla-derived href as a relative path (`/${alias}`) — never prefixed with the marketing domain.
2. **Strict 404 Enforcement:**
   - In `app/[...slug]/page.tsx`, if an article is **NOT** found via the Joomla API, Next.js immediately calls `notFound()` to display the 404 page. It will **NEVER** navigate out to the live external website or display dummy fallback placeholders.

---

## 4. Multi-Brand Site Switching (the `webHost` cookie)

This Joomla install hosts several brands from one shared API (confirmed by reading `configuration.php` on the Joomla server): each brand has its **own database** (`joomla_GAH`, `joomla_IAH`, `joomla_GAH_AU`, ...), and the request-time cookie `webHost` picks which one the API responds from.

- `services/joomlaService.ts` sends `Cookie: webHost=<value>` on every request.
- The value comes from `JOOMLA_SITE_COOKIE` in `.env.local` (defaults to `GAH`).
- **To repoint this app at a different brand, change one env var** — no code changes needed. Valid values (from Joomla's own `configuration.php`): `GAH`, `IAH`, `GAH_AU`, `IAH_AU`, `AP`, `AD`, `DD`.
- The home page's article ID also differs per brand (GAH's is article `1`, IAH's is article `11`) — `joomlaService.getHomePage()` resolves this dynamically from the live "Home" menu item's link rather than hardcoding an ID, so switching brands doesn't require touching that either.

### A critical Joomla API quirk

Joomla's Native Web Services API returns **406 Not Acceptable** for a plain `Accept: application/json` header — it only accepts `Accept: application/vnd.api+json`. Getting this wrong makes every API call silently fail and the app falls back to `data/fallback*.ts` with no visible error (this was the root cause of the app looking "static" before this header was fixed). If Joomla calls start failing again, check this header first.

---

## 5. Data Flow & API Contracts

### End-to-End Data Pipeline

```text
Joomla Admin Panel (Edits Articles, Categories or Menus)
   │
   ▼
MySQL Database (joomla_GAH: JM_content & JM_menu)
   │
   ▼
Joomla Native Core REST API (/api/index.php/v1/content/articles, /menus/site/items)
   │  (Bearer <JOOMLA_API_TOKEN> + Cookie: webHost=<JOOMLA_SITE_COOKIE>)
   ▼
Next.js Service Manager (services/joomlaService.ts)
   │
   ▼
Next.js Server Components & Route Handlers (app/layout.tsx, app/page.tsx, app/[...slug]/page.tsx)
```

### How the navigation tree is built

`/menus/site/items` has no working server-side filter (`filter[parent_id]` etc. are accepted but ignored), so `joomlaService` paginates through every page via `links.next` and builds the tree client-side. Two things it filters out:

- **Duplicate legacy site trees.** The menutype contains several old duplicate top-level trees left over from previous revisions, distinguishable only by a numeric alias suffix (`home-4`, `services-9`, ...). Only the unsuffixed alias is the live tree; suffixed roots (and everything under them) are excluded.
- **Unpublished/trashed items** (`published !== "1"`).
- **Items Joomla itself hides from menus.** `attributes.params.menu_show === 0` is Joomla's own "don't render this in a menu" flag — it's how the real site keeps `Login`, `Reset Password`, `404`, etc. working as routes without showing them as nav tabs. See §8 for the full explanation.

Each top-level item becomes a 2-level mega menu (`categories` → `items`) if its children themselves have children, a flat dropdown (`subItems`) if not, or a plain link otherwise. Only links the `[...slug]` route can actually resolve are included as leaves — i.e. Joomla `component` items pointing at `view=article`, plus `type: "url"` items. Category-listing views, the search component, and the XML sitemap component are excluded because the Native REST API has no working way to list a category's articles efficiently (`filter[category_id]` is likewise a no-op), so a listing page for those isn't backed by anything real.

**A category's items include both the subject-hub nodes and their own children — a union, not one or the other.** E.g. under "Assignment Services", "Management" is itself a real page *and* has 48 children of its own (Change Management, Accounting, ...); the mega menu shows "Management" as an item *and* flattens its children in too. This matches the live site's actual per-category counts exactly (verified against the count badges Joomla shows next to each category name — Summary Writing Services: 1, Thesis Help: 2, Homework Help: 5, Dissertation Help: 22, etc. all match precisely). An earlier version of this logic stopped at the subject level only, which undercounted every category with subject-level grouping. There's no cap on how many items a category can produce (`collectResolvableLeaves`'s `limit` is a high safety ceiling, not a curation choice) — "Assignment Services" alone is genuinely ~200 real items, and the dropdown's own `max-h-[360px] overflow-y-auto` handles the length.

Footer columns (`Our Company`, `Assignment By Countries`, `Let Us Help You`, `Top Assignment Searches`) are derived from the same live tree by locating the `Company`, `assignment-by-countries`, and `Policies` menu nodes and reading their children — not hardcoded link lists.

### 1. GET `/api/navigation`

Returns `NavigationResponse` JSON:

```typescript
interface NavigationResponse {
  header: HeaderData;
  footer: FooterData;
}
```

### 2. GET `/api/home`

Returns `HomePageResponse` JSON:

```typescript
interface HomePageResponse {
  articleTitle?: string;
  articleContentHtml?: string; // omitted if the CMS article is a legacy page-builder template, not prose — see below
  hero: { announcementBadge: string; headingPrefix: string; headingHighlight: string; headingSuffix: string; subtitle: string; trustBadges: TrustBadgeItem[]; calculator: CalculatorConfig };
  servicesSection: { title: string; subtitle: string; services: ServiceCardItem[] };
  whyChooseUsSection: { badgeText: string; title: string; features: WhyChooseUsItem[] };
  testimonialsSection: { title: string; subtitle: string; testimonials: TestimonialItem[] }; // rendered only when non-empty — no fabricated reviews
  bottomCtaBanner: { title: string; subtitle: string; buttonText: string; buttonHref: string };
}
```

**Why `articleContentHtml` can be omitted:** GAH's "Home" article (id `1`) is a legacy page-builder blob (slider banners, unresolved PHP path tokens like `image_pathhomebanner`), not a plain content blurb — dumping it via `dangerouslySetInnerHTML` produces broken image requests. `joomlaService.getHomePage()` detects this (`looksLikeUnresolvedTemplate`) and skips it, relying on the hero/services/why-choose-us sections instead. Regular `[...slug]` content articles don't have this problem — only the home article does.

### Services-section page chrome (trust badges, rating strip, order sidebar)

On the live site, pages under Services (e.g. `/kaplan-assignment-help`) render with a trust-badge row (Free Plagiarism Report / Free Revision / 24*7 Availability), a Google/Sitejabber/Trustpilot rating strip, and a sticky order-form sidebar — none of which exists in the article's CMS content. It's Joomla's `templates/GAH/html/com_content/article/service.php`, an alternate article layout scoped to the Services category tree; the REST API only returns the article's `text` field, not which layout Joomla would render it with.

**Detecting which pages get it:** every Services-tree article carries a `page-layout` custom field (empty/absent on every other page — verified against `about-us`, `privacy-policy`, etc.). `joomlaService.getArticleBySlug()` reads it into `JoomlaArticle.isServicePage`. Note the REST API serializes this field inconsistently — a single value comes back as `{"1": "New"}`, multiple as `["Old"]` — so the check is shape-agnostic (`Object.keys(...).length > 0`), not an array check. This field is **only present on the search endpoint** (`/content/articles?filter[search]=...`), not the single-resource one (`/content/articles/{id}`), which is why `getArticleById()` (used for the home article) always sets `isServicePage: false`.

**Sidebar heading text:** the live template resolves this in two layers, both replicated in `app/[...slug]/page.tsx`:
1. A CMS custom field, `banner-form-title` / `banner-form-subtitle` — exposed by the API, but empty on every article checked so far, because...
2. ...`service.php` unconditionally overwrites it afterward with a hardcoded `if ($this->item->alias == '...')` chain for 8 specific aliases (`dissertation-help`, `thesis-help`, `homework-help`, `case-study-help`, `coursework-help`, `essay-writing`, `research-paper-help`, `summary-writing-services`), falling back to the generic "Professional Academic Help" / "Buy Assignment Writing Help Online" for everything else. That hardcoded table is copied verbatim into `ALIAS_BANNER_TEXT` in `app/[...slug]/page.tsx`, since there's no CMS-exposed source for it — it only exists in the PHP template. `bannerFormTitle` (the CMS field) still wins if it's ever actually populated for a given article.

**The banner is a full-bleed section with the order form inside it.** The live `.banner_landing_pages` rule puts its gradient on the whole `<section>`, with `.first-sec-left` (54%) and `.side-bar` (34%) sitting side by side within it — not a green box on the left and the form on a separate white background. The breadcrumb renders *below* this section, not above it.

**Banner subtitle** (the line under the H1) is the article's `attribs.alternative_readmore`. The REST API exposes it on neither the search nor the single-article endpoint, so it comes from the bridge's `type=article-meta&alias=...` and is only fetched for Services pages (`JoomlaArticle.bannerSubtitle`).

**Trust badges are two-line.** `.feature-box .content span { display: table }` on the live site forces the badge word onto its own line above the label — "FREE" over "Plagiarism Report", "Free" over "Revision", "24*7" over "Availability" — rather than a single inline run.

**Banner is a separate block from the article content**, not merged into one card — `app/[...slug]/page.tsx` renders them as two stacked `<div>`s inside the left column, matching how the live site keeps `section.banner_landing_pages` and `section.inner_services` as distinct sections.

**Trust-badge and rating-platform icons** (`public/icons/`) are the real assets copied from `templates/GAH/images/` on the Joomla server (`plagiarism.svg`, `revision.svg`, `availability.svg`, `google.svg`, `sitejabber.png`, `trustpiolet.png` → `trustpilot.png`) — not generic Lucide icons standing in for them. This row is brand-wide template chrome identical on every Services page, not CMS content, so unlike everything else in this app it's a genuinely static asset, checked into the repo rather than fetched.

**Colors are copied from the compiled CSS, not approximated with Tailwind's palette** — the live `.banner_landing_pages` rule (`templates/GAH/css/common.css`) uses exact gradients/hex values that don't map to any standard Tailwind color, so `app/[...slug]/page.tsx` sets them via inline `style` instead of forcing a close-but-wrong utility class:
- Banner background: `linear-gradient(94.59deg, #F3FBE9 0%, #E1ECD3 22.19%, #E2F0D1 50.44%, #ECF2E5 73.85%, #F4FDE9 96.63%)`
- Rating strip background (one shared pill, not three separate boxes): `linear-gradient(90deg, #FFFFFF 2.98%, #FFE7DA 49.75%, #BFFFEA 98.36%)`
- "FREE" badge: `#C80B0B`; title text: `#3F3F3F`
- Sidebar (`<OrderCalculator containerClassName="bg-[#F5FFE9] border-[#5C7A36] ...">`): background `#F5FFE9`, border `#5C7A36` — `OrderCalculator`'s `containerClassName` prop exists specifically so this page can override the component's default white styling without changing how it looks on the homepage, where it's reused with the default.


### Services-page right sidebar

On the live site the whole right column is one Joomla module — **"Rightbar Service"** (`mod_custom`, id `115`, position `user6`), pulled into `service.php` as `$custom_rightbar_section`. `components/common/ServiceSidebar.tsx` reproduces its markup and class names so the template's own CSS (extracted to `app/service-sidebar.css`) styles it. Where each block comes from:

| Block | Source | Live / local |
|---|---|---|
| Promo strip | Module markup. The active variant is `?code=offer_code` + `service_image`, substituted from the central `crm_offers` table; every seasonal variant in the module is commented out. | **`crm_offers` does not exist on this server**, so the strip uses the module's own `images/sidebar/service-sidestrip-gah-2025.png` with code `EXTRA25` — which is the offer the live page is currently running. Copied to `public/images/service-offer-strip.png`. |
| "Paid FREE Features" | `$static_free_feature_rightbar` in `default.php` (substituted into the module via the `static_free_feature_rightbar` token). | Fully reproduced. Prices are `ContentHelper::convertCurrency(5.8 * 50)` etc. — see below. |
| "Our Samples" | Module markup (`.rightbar_samples`), 6 hardcoded links. | Fully reproduced. The CSS targets `ul > li > a.samples`, so the markup must keep the `ul`/`li` wrappers. |
| "Explore Our FREE Tools" | Module markup (`.sidebar-tools-adv`), 11 hardcoded links. | Fully reproduced. `.tools-inner-box` is `max-height:275px; overflow:auto` in the real CSS, so it scrolls rather than showing all 11 — that's the live behaviour, not a bug. |

**Prices.** `ContentHelper::convertCurrency($amt)` is `$_SESSION['order_currency'] . ' ' . round($amt / $_SESSION['selected_price'], 2)` — the currency **code**, then the amount divided by `selected_price`. That divisor (`CalculatorConfig.currencyDivisor`) is per-country on the live site, derived from `currency_values` in the central database. That table **is** readable here (`mr_central.currency_values`), but the base `price`/`static_price` it's combined with are not, so the value in `fallbackHomeData` was solved from the live sidebar's own figures. It reproduces all six feature prices and the total exactly.

### `STRIPn` placeholder tokens in article content

Some articles (authored through the legacy page builder) contain literal unresolved tokens like `<p>STRIP1</p>` in their `text` field — e.g. `kaplan-assignment-help` has `STRIP1` and `STRIP2` mid-article. These aren't broken content; Joomla replaces them at render time with a promo block pulled from `com_banners` (the Banners component), specifically banners in the **"Page strips"** category, matched by the banner's `name` field (a banner literally named `STRIP1` exists, `catid: 8`).

`joomlaService.resolveStripPlaceholders()` replicates this generically (not hardcoded to `STRIP1`/`STRIP2` — any `STRIPn` name in that category resolves) via a two-step lookup:
1. `/banners?page[limit]=200` — list endpoint, gives `name` → `id` for every "Page strips" banner. **This endpoint's fieldset does not include `description`** (the actual promo HTML) — that's the bug that made this silently return nothing on the first pass.
2. `/banners/{id}` — single-resource endpoint, only fetched for banners whose name actually matched a token found in the current article, returns the real `description` HTML to substitute in.

**Matching is driven by the real banner names, not a token pattern.** An earlier version matched a `STRIP\w*` shape, which silently missed every strip whose name didn't fit it — `NEWSTRIP_ONE`..`NEWSTRIP_FIVE`, `AI_STRIP`, `whatsapp-scanner-strip` all rendered as literal text in article bodies. The resolver now builds its matcher from the names actually present in the "Page strips" category (longest first, so a name that prefixes another can't win).

**Strip HTML links assets relative to the Joomla root** (`src="templates/GAH/images/..."`), which would resolve against the Next.js origin and 404. `rewriteCmsAssetUrls` rewrites those onto `siteBaseUrl` (derived from `JOOMLA_API_BASE_URL`).

The returned HTML uses the live site's own class names. The newer strips' real CSS is copied verbatim (base, non-media-query rules) from `templates/GAH/css/questions.css` into `app/strips.css`; `app/globals.css` keeps hand-written rules for the older `.common-strip` blocks. That fallback button rule is scoped to `.common-strip .strip-btn` on purpose — unscoped, its orange background leaked onto the newer strips' outline buttons and made orange-on-orange text.

Pages under Services get the two-column layout (badges/rating strip + `<OrderCalculator>` sidebar, reusing the homepage's calculator config); every other section renders the plain article layout.

---

## 6. Direct Server Helper

In Server Components like `app/layout.tsx` and `app/page.tsx`, import direct getters from `services/joomlaService`:

```typescript
import { joomlaService } from "@/services/joomlaService";

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  const navigationData = await joomlaService.getNavigation();
  return (
    <html>
      <body>
        <Header data={navigationData.header} />
        <main>{children}</main>
        <Footer data={navigationData.footer} />
      </body>
    </html>
  );
}
```

---

## 7. Guidelines for Adding Future Routes & Features

1. **Page Creation:**
   - For static/custom pages, place under `app/<route-name>/page.tsx`.
   - For Joomla CMS dynamic pages, `app/[...slug]/page.tsx` automatically resolves any article alias created in Joomla Admin. If the slug does not exist, `notFound()` triggers the 404 page.
2. **Component Conventions:** Place reusable UI primitives in `components/common/`, homepage components in `components/home/`, and layout structures in `components/layout/`.
3. **Interactive Components:** Mark client-side stateful components with `"use client";` at top of file (e.g. `NavDropdown`, `MobileMenu`, `OrderCalculator`). Keep layout and page boundaries as Server Components for optimal SEO.
4. **Accessibility:** Ensure all interactive elements have keyboard listeners (`Enter`, `Space`, `Escape`), `aria-expanded`, and descriptive `aria-label` attributes.
5. **No hardcoded brand content:** contact details, social links, and site name are read from env vars (`NEXT_PUBLIC_*`) or fetched from Joomla — never inline a brand string in a component. This is what makes switching brands (§4) a one-line env change.
6. **Brand Styling & Palette** (pulled from GAH's live template SCSS, `templates/GAH/scss/mixin/_variable.scss`, not invented):
   - Primary Brand Green: `#5C7A36` (top bar, links, active nav states)
   - Primary CTA Orange: `#f4611e` (Order Now / primary buttons — matches the real site's orange CTA, distinct from the green brand accent)
   - WhatsApp Green: `#1faf38` (WhatsApp's own brand color, unrelated to GAH's palette)
   - Dark Slate Base: `#0f172a` (slate-900 / slate-950)

---

## 8. Known Limitation: category & contact listing pages aren't supported

A menu item's `attributes.params.menu_show` field is Joomla's own signal for whether an item should render in a menu at all (it's `0` for system routes like `Login`, `Reset Password`, `404`; `1` for normal content). `joomlaService` filters on it, so the header/footer only show what Joomla itself marks as visible — no hardcoded title/alias blocklist involved.

That still leaves a real gap: some of GAH's real top-level nav items point at Joomla views this app can't render:

- The Native REST API's `filter[category_id]` / `filter[parent_id]` params are accepted but silently ignored (confirmed by testing), so there's no efficient server-side way to fetch "articles in category X".
- Fetching the full article table to filter client-side isn't viable either — it 500s past a few thousand rows, and individual article records don't even carry a `category_id`/`category_title` field to filter by.
- `/contacts/contacts` (needed for a Writers directory) isn't exposed by this API at all (404s).

Because of this, `joomlaService` only includes menu items pointing at single `com_content` articles or custom URLs (see `isResolvableLink`) — category/contact listing items are left out of the nav rather than linked to a page that would 404.

**Three of these turned out to be recoverable, and `getNavigation` special-cases them** (see the "Samples"/`buildFreeResourcesItem` block):
- **Samples** — the menu item points at a category view, but a real article happens to share its alias (`/free-samples`), so it's added using that confirmed match.
- **Questions** — same trick as Samples. The nav item points at category `314`, which genuinely has `0` published articles (`SELECT COUNT(*) FROM JM_content WHERE catid=314`) — but a standalone article exists whose own alias is literally `questions`, unrelated to that empty category. `joomlaService.ts` hardcodes the nav entry (`{ id: "questions", href: "/questions" }`); the generic `[...slug]` catch-all resolves it the same way any other article alias resolves. **Note: on this brand's database that article's `text` field is placeholder content (`"dhdhdhfgh "`), not real FAQ copy** — the route and data flow are correctly wired, but the CMS record itself needs real content entered in Joomla Admin before the page reads like the live site's.
- **Free Resources** — "Tools" and "Referencing" are real branches with real working children (paraphraser, plagiarism checker, MLA/APA/Harvard referencing, ...), just individually flagged `menu_show: 0` so the generic tree walk skips them. The live site still surfaces them under a hand-built "Free Resources" grouping, which `buildFreeResourcesItem` reassembles from the same real nodes.

**Blog and Writers are resolved via a custom bridge** (below) rather than the shared-alias trick, since they're genuine category/contact listings with no single backing article.

### Category listing bridge

`api-bridge/category-listing.php` on the Joomla server (not under Joomla's own `api/` folder) is a small, purpose-built read-only endpoint that does what the REST API can't: list articles/contacts by category. It connects to the DB directly rather than going through Joomla's routing.

- **Auth:** same Bearer token as the main API, checked with `hash_equals`.
- **Site selection:** same `webHost` cookie as the main API, mapped to the matching database (mirrors `configuration.php`'s own per-brand `$config` array — same host/credentials, only the DB name changes).
- **Whitelisted types only** — `type=blog|writers|reviews|samples|article-meta|order-config|pricing|site-stats|samples-sidebar|offers|related-services|home-banner`. Category/table/module ids are hardcoded server-side per type; the client can never pass an arbitrary id. This is deliberately narrow, not a general-purpose query endpoint.
- **`type=site-stats`** → the real `tot-reviews` / `tot-writers` / `tot-ratings` custom fields on the site's own contact record (`#__fields_values`, `item_id=1`), matched by name because the live template's hardcoded field ids (6/7/8) differ per brand database — same reasoning as `pricing`. These back the real "4500+ Experts Writer" / "Rated 4.8/5 based on 5087 reviews" figures, and `getFooterRatingInfo()` now prefers them over averaging a sample of the reviews table.
- **`type=samples-sidebar`** → the raw stored `content` of the real "Samples listing sidebar" module (`#__modules` id 118, `mod_custom`, position `user8`). Joomla's Native Web Services API has **no `/modules` endpoint at all** (verified — it 404s), so this is the only way to read module content. `/free-samples/[subject]` renders that HTML as stored and only substitutes the module's own placeholder tokens (`experts_count`, `category_sample_details`, `dynamic_rating`, `dynamic_review`, `dynamic_writer_section`, `order_now_url`, `root_url`, `image_path`), styled by `app/samples-sidebar.css`. That means editing this module in Joomla Admin changes that sidebar with no code change. The one exception is the seasonal offer image/code (`Sidebar_image`/`offercode`), which the live site substitutes from the central `crm_offers` table that doesn't exist on this server — those fall back to the same static EXTRA25 asset the Services sidebar uses.
- **`type=blog`** → `JM_content WHERE catid=190` (real, ~267 published posts). **`type=writers`** → `JM_contact_details WHERE catid=172` (real, ~145 published profiles). **`type=reviews`** → `JM_writers_review WHERE state=1 AND site_short_name=<brand>` (real, ~874 published customer reviews) — powers the homepage testimonials section (`getHomePage`), which only renders when reviews are actually returned rather than falling back to invented ones.
- **No customer name or university field exists** in `JM_writers_review` — only which writer was reviewed. `TestimonialItem` reflects that (`writerName`, not `studentName`/`university`) rather than fabricating a reviewer identity.
- `joomlaService.fetchFromBridge()` / `getBlogPosts()` / `getWriters()` / `getReviews()` call it from `JOOMLA_BRIDGE_URL` (`.env.local`). `/blog` and `/writers` are dedicated listing pages in `app/[...slug]/page.tsx` (paginated, `?page=N`), separate from the generic single-article flow.
- This only works where the Next.js app can reach the Joomla server's DB directly — fine in this local dev setup (same network), but worth re-checking before any production deployment where that network path may not exist.

---

## 9. Provenance: what comes from where

Every piece of the UI, and which of the four sources it comes from:

- **API** — Joomla Native REST API (live per request)
- **Bridge** — `api-bridge/category-listing.php` (live per request; things the REST API can't serve)
- **Template** — copied verbatim from the Joomla template's PHP/CSS on disk (static there, so static here)
- **Local** — an asset file copied into `public/`

### Page content

| Route | What renders | Source |
|---|---|---|
| `/` | Hero, services, why-choose-us, bottom CTA | **API** `/content/articles` (home article, resolved from the live "Home" menu item). Page-builder blob is detected and skipped — see §5. |
| `/` seasonal banner | The `.loading-home-banner`/`.slider-banner` image(s) beside the calculator (e.g. the Christmas "UPTO 50% OFF / HAPPYHOLIDAY" art) | **API**, real CMS markup — `joomlaService.resolveHomeArticleTokens()` extracts the `.slider-section` block's banner markup into `HomePageResponse.heroBannerHtml` (kept separate from `articleContentHtml` so `app/page.tsx` can render it as a flex sibling of `<ServiceOrderForm>` inside one real `<section class="container slider-section">`, matching `homenew.scss`'s actual layout, instead of the banner and form ending up in two disconnected `dangerouslySetInnerHTML` blocks). |
| `/` testimonials | Real customer reviews | **Bridge** `type=reviews` (`JM_writers_review`) |
| `/[any-alias]` | Article title + body HTML | **API** `/content/articles?filter[search]=` |
| `/[services-alias]` | Banner title, badges, rating strip, order form, sidebar | **Template** chrome + **API** article (see rows below) |
| Banner subtitle | Line under the H1 | **Bridge** `type=article-meta` → `attribs.alternative_readmore` (not exposed by the REST API) |
| Sidebar form heading | Per-alias title/subtitle | **Template** `ALIAS_BANNER_TEXT`, copied from `service.php`'s hardcoded `if ($this->item->alias == ...)` chain |
| `/blog` | Post list, paginated | **Bridge** `type=blog` (`JM_content` catid 190) |
| `/writers` | Writer profiles, ratings, photos | **Bridge** `type=writers` (`JM_contact_details` catid 172 + `JM_writers_review` join) |
| `/free-samples` | Subject browser | **API** `/content/categories` (real `count_published`) |
| `/free-samples` counts | The number beside each subject | **Template** — the live site multiplies the real count (`>100 ? ×15 : >40 ? ×10 : >20 ? ×4`), replicated in `SampleCategoryGrid` |
| `/free-samples` body copy | Promo strip + long SEO text | **Template** `default_children.php`, gated on `category->id == 185` |
| `/free-samples/[subject]` | Samples in a subject | **Bridge** `type=samples` |
| Header nav | Services mega menu | **API** `/menus/site/items` |
| Header nav | Samples · Questions · Blog · Writers · Free Resources | **Template** — hardcoded in `mod_menu/default.php`, not menu data (see §8) |
| Footer columns | Company / Countries / Policies / Top searches | **API** same menu tree |

### Strips and promo blocks

| Item | Source |
|---|---|
| `STRIPn`-style tokens in article bodies | **API** `com_banners` "Page strips" category. Matching is driven by the real banner names, so `NEWSTRIP_ONE`…`FIVE`, `AI_STRIP` and `whatsapp-scanner-strip` all resolve. |
| Their CSS | **Template** `questions.css` base rules → `app/strips.css` |
| Their images | **API**-returned HTML, with Joomla-relative `templates/...` URLs rewritten onto the Joomla origin |
| In-article `class="button"` CTAs | **Template** `.allinner_main .inner_services .left .button` |
| In-article link colour `#f58e0e`, table borders | **Template** `questions.css` / `content.css` |

### Forms

| Field / part | Source |
|---|---|
| Form markup, classes, layout | **Template** — same sidebar form in `service.php` and `default.php` |
| Styling (two variants) | **Template** — services sidebar from `common.css`, homepage hero from `homenew.css` |
| Subject / pages / urgency options | `CalculatorConfig` (app config) |
| "Select Services" options | **Template** — the 15 labels rendered in `index.php`. Display-only; the live list is filled client-side from the customer panel. |
| Currency prefix | **Template** — `$_SESSION['order_currency']`, the ISO **code** (`INR`), not the symbol |
| Price maths | **Template** — discounted figure is the base; struck price is `round(100 * value / (100 - discount), 2)` |
| Sidebar "FREE Features" prices | **Template** — `convertCurrency(amt) = code + round(amt / selected_price, 2)` with base amounts `5.8*50` … `115.8*50` |
| `basePricePerPage`, `currencyDivisor` | ⚠️ **Stand-ins.** The real values come from `row_discount.static_price` + `currency_values` in the central database. `mr_central.currency_values` is readable; the base price is not. Both were solved from the live page's own figures and reproduce them exactly, but are **not** per-country as the live site is. |
| Order Now button | Sends to the real order URL **without** the visitor's selections — the live form POSTs internal ids that couldn't be confirmed |

### Icons and images (all **Local**, in `public/`)

| File(s) | Origin |
|---|---|
| `logo.svg`, `logo-white.png` | `templates/GAH/images/` |
| `phone.svg`, `mail.svg`, `whatsapp.svg` | Inline SVGs in `index.php`'s top strip, saved as files |
| `phone-dark.svg`, `mail-dark.svg` | Same paths, recoloured for the light mobile drawer |
| `facebook/twitter/instagram/pinterest/youtube.png` | `templates/GAH/images/footer/*-footer.png` |
| `play-store-badge.png`, `app-store-badge.png` | `Play-store.png`, `App-store.png` |
| `dmca-protected.png`, `payment-methods.png` | `dmca_protected.png`, `payment-secure-footer.png` |
| `plagiarism.svg`, `revision.svg`, `availability.svg` | `templates/GAH/images/` — Services banner badges |
| `google.svg`, `sitejabber.png`, `trustpilot.png` | `templates/GAH/images/` — rating strip logos |
| `samples-*.svg` (4) | Inline SVGs in `samplelanding.php`'s `.sample-feature` block |
| `pen-doodle-1.svg`, `pen-doodle-2.svg` | `pen-icon-1.svg`, `pen-icon-2.svg` — Samples banner corners |
| `breadcrumb-home.svg` | Inline SVG in `mod_breadcrumbs/default.php` |
| `breadcrumb-arrow.png` | Cropped from `sprite.png` at the CSS's own offset (`-474px -82px`, 10×10) |
| `down-arrow.svg`, `offer-watch.gif` | `templates/GAH/images/` — form select arrow, discount clock |
| `sidebar-writers.svg`, `sidebar-plagiarism.svg` | Inline SVGs in the home article's `footer-sidebar` block |
| `man-with-diary.png` | `templates/GAH/images/` — Samples promo strip |
| `service-offer-strip.png` | `images/sidebar/service-sidestrip-gah-2025.png` |
| `sprite.svg`, `sprite1.png` | `templates/GAH/images/` — referenced by the sidebar CSS |
| `defaultWritter.png` | Fallback for writer photos whose CMS paths 404 |

### Not sourced from the CMS

Two things in the UI are **not** traceable to the Joomla install, and are flagged here rather than buried:

1. **Rating-strip scores** (Google 4.7, Sitejabber 4.3, Trustpilot 4.5) — the live template renders star graphics, not numbers. These are the platforms' publicly listed scores, not a CMS field.
2. **`basePricePerPage` / `currencyDivisor`** — see the Forms table above.

### Genuinely unavailable locally

| Wanted | Blocker |
|---|---|
| Live promo-strip image + offer code | `crm_offers` does not exist in any database on this server |
| Per-country pricing | `row_discount.static_price` and the `$price` base are in the central DB |
| Writers' "Orders Finished / In Progress" | No orders table in `joomla_GAH` |
| Questions listing | `SELECT COUNT(*) FROM JM_content WHERE catid=314` → 0 |
