A bilingual (English/Arabic) marketing and informational website for Jeddah International College, a private Saudi higher-education institution. Nearly every page is server-rendered over a headless Statamic CMS reached through GraphQL — there are no student accounts, no LMS, and no payments; the only authenticated surface is a single password-gated internal-document portal.

The site runs on Next.js 15 (App Router) and React 19, deployed on Vercel, with content editors managing nearly all copy and media through a headless Statamic CMS. A single catch-all [locale]/[slug] route resolves a CMS-authored blueprint field to one of roughly 15 feature components, so most new pages ship without a code deploy.
Recent work on this branch focused on keeping the site current rather than building new features: migrating the whole locale-tree from Next.js 14's synchronous params/cookies APIs to Next 15's async versions, fixing a static-params bug Next 14 had silently tolerated, and producing a 16-document, file-and-line-cited architecture knowledge base of the existing codebase to make future changes safer.
Migrated the site from Next.js 14 to 15: updated every layout/page/generateMetadata across the locale tree to await the new async params/searchParams/cookies APIs, and fixed a generateStaticParams bug that Next 14 had silently tolerated (an inner .map callback whose return value was dropped, so no page was ever actually receiving real static params).
Extracted an ssr:false dynamic import out of a Server Component into a new sibling Client Component, matching Next.js 15's stricter rule that ssr:false imports can no longer live inside a Server Component.
Authored a 16-document, file-and-line-cited architecture knowledge base (docs/architecture/) covering runtime, routing, rendering, data flow, GraphQL, CMS mapping, components, hooks, state, CSS, and risk — produced by direct static analysis of the existing codebase.
Simplified the site's AOS scroll-animation hook: removed an arbitrary 1500ms delay before initialization (AOS already hides elements via CSS immediately, so the delay only left content blank longer) and added a route-change re-scan so client-side navigations correctly re-trigger entrance animations.
Ongoing bilingual content maintenance, e.g. correcting an Arabic program-track label in the admissions tuition-fee table.
The site's one authenticated surface — a password-gated internal-document page — checks its password against a value stored in the CMS rather than a dedicated credentials system, fetched over the same GraphQL pipeline as any other page content. This lets a small internal-file gate reuse existing CMS/GraphQL plumbing and lets non-engineers rotate the password without a deploy — at a real trade-off in exactly how that credential is stored and compared, which isn't detailed further here.

Nearly every page's rendering is dispatched by matching a CMS-authored blueprint string in two separate places — a data-fetch switch in getData.js and a component-selection switch in [locale]/[slug]/page.js — rather than a single shared enum or table driving both. Each switch stays simple and self-contained to read in isolation, which suits a slow-changing set of roughly 15 page types, at the cost of the two switches being able to silently desync: a typo or missed case in one leaves data fetched with nothing to render it, with no compiler or lint check tying the two strings together.
CMS-blueprint pages skip build-time static generation entirely (generateStaticParams resolves to an empty array) in favor of rendering on first request and caching the result for 3 days via ISR (revalidate = 259200). This avoids needing to enumerate every CMS slug/locale combination at build time — which would mean a CMS call during every build and a rebuild for every new page — at the cost of a slow, uncached first hit for any not-yet-requested page/locale combination.
All CMS content is fetched exclusively inside async Server Components via Apollo's server-only client factory — there is no ApolloProvider or useQuery on the client anywhere in the app. This avoids shipping a GraphQL cache to the browser and keeps data-fetching server-side per Next.js App Router conventions, at the cost of one deliberate escape hatch: news-listing pagination re-fetches through a separate raw-axios GraphQL call instead, since a full Server Component round-trip would be too heavy for click-driven pagination — leaving two parallel GraphQL transports in the codebase instead of one.
Arabic/English mirroring is built from three overlapping hand-written layers — the native dir attribute, custom .rtl/.ltr descendant-selector CSS, and per-element dir overrides — rather than adopting a Tailwind RTL plugin. This gives fine-grained control over specific mirrored elements (SVG icons, Swiper controls, list markers), at the cost of real redundancy: the same locale check drives both the dir attribute and a parallel .rtl/.ltr class doing the same job, and 100+ .rtl/.ltr selector occurrences have accumulated in a single 1,549-line global stylesheet with no plugin abstraction to lean on.

CMS-publish revalidation is implemented as a single route that calls revalidatePath("/", "layout") on every publish event, invalidating the entire site's rendered-page cache regardless of what actually changed — rather than using Next's revalidateTag for targeted, per-page invalidation (confirmed unused anywhere in the codebase). This is far simpler to wire from one CMS webhook and guarantees no stale page ever slips through, at the cost of every single content edit busting the whole site's cache, plus a secret check against a hardcoded literal string rather than an environment variable.
While fixing the Next.js 15 generateStaticParams bug, the trade-off was returning an explicit empty array rather than using the migration as an opportunity to wire up real build-time static params for the [locale]/[slug] tree. This preserves the exact production behavior already running (pages render on first request, then get cached for 3 days via ISR) instead of introducing new pre-rendering edge cases in the same change that was already touching every route in the tree.
Rather than converting the whole single-education/Index.js Server Component to a Client Component to satisfy Next 15's new restriction on ssr:false dynamic imports, the trade-off was extracting just the two swiper widgets that needed it into a new sibling "use client" file. This keeps the page's CMS data-fetching server-side and isolates the client-only requirement to exactly the two components that actually need it.
The trade-off was removing a hardcoded 1500ms setTimeout before Aos.init() and initializing immediately instead. AOS already hides [data-aos] elements via CSS the moment the page loads, so the delay wasn't smoothing anything — it was only leaving above-the-fold content blank for longer before animations could fire.
The trade-off was hand-copying validation/submit/reset state five times inside one 2,107-line component (states1..5, errors1..5, handleSubmit1..5) for the help-and-support page's five form variants, instead of adopting the generic src/form/useForm.js hook that already exists in the repo. It kept each variant's quirks locally editable without a shared abstraction leaking cross-form assumptions, at the cost of the same email-regex validation copy-pasted five times and useForm.js becoming dead code with zero importers.
Nearly every CMS data-hook and API route catches its error, logs it with console.error/console.log, and returns undefined or a generic JSON error rather than re-throwing. A broken GraphQL field degrades to a blank/optional-chained render instead of a 500 — good for resilience against a flaky headless CMS, at the real cost that failures are invisible in production beyond server console logs, with no user-facing error state or alerting.
0
unit tests
0
E2E scenarios
next/core-web-vitals ESLint config is used, but jsx-a11y/alt-text is explicitly disabled in .eslintrc.json; repo-wide, only 4 files use aria-*/role attributes. Locale-driven lang and dir attributes are set correctly in exactly one place (the root layout) with no per-route override, a genuine if fragile consistency win, but custom accordion/tab-toggle widgets (FAQ accordion, admissions tab nav) use imperative DOM class toggling with no ARIA roles or keyboard handling found. A live Lighthouse accessibility score of 77/100 (both desktop and mobile) corroborates a partial-but-incomplete implementation rather than either extreme.
The only objective measurements available are Lighthouse audits run against the live production site: desktop scores 33/100 on performance (driven by a 6.1s LCP and a 0.70 cumulative layout shift), while mobile scores 64/100 (a much slower 16.0s LCP, but a good 0.006 CLS and 100ms TBT) — accessibility sits at 77/100 on both, best practices at 96/92, and SEO at 92/92. No analytics, conversion, or visitor data exist to measure business impact, so this results summary is limited to what Lighthouse can verify: the site is functionally complete and indexable, but has real, measurable performance headroom on both form factors, concentrated in largest-contentful-paint time rather than JS execution or, on mobile, layout stability.
Visit the live site ↗ (opens external site)This codebase has no automated tests and no CI/CD pipeline of any kind; every routing/rendering/CMS-mapping claim in its own architecture docs is explicitly caveated as static-analysis-only, unverified at runtime. There's a real, if narrow, security-relevant gap worth fixing: how the one authenticated route (/employees-gate) stores and compares its credential — a low-blast-radius issue (it gates internal PDFs, not user data or payments) but a real one, and deliberately not detailed further here. Beyond that, the codebase shows the normal accumulation of a CMS-driven marketing site built and extended over roughly two years without a cleanup pass: two fully dead page routes (a debt page and a terms-and-conditions page), a generic form hook built and never adopted, a paid lightbox library licensed and used exactly once while six other call sites hand-roll equivalent logic, and a debug-only file write left in production that isn't even gitignored. None of this is unusual for a site of this age and scope, but a focused cleanup pass — plus the LCP-driven performance headroom the Lighthouse audit surfaced — would be the highest-leverage next work here, ahead of any new features.