The public site for Saudi Banks' Media and Banking Awareness Committee (established 2006 under a SAMA circular): a bilingual (Arabic/English) Next.js site combining committee content, a fraud-awareness "Be Careful" campaign with an interactive quiz, and an aggregated banking-news feed, backed by a headless Statamic GraphQL CMS.

This is a long-running, multi-engineer build — git history on this repo goes back to September 2023, with several agency engineers contributing across nearly 300 commits. The site has gone through a Statamic 4→6 CMS migration, a Next.js 14→15 upgrade, and a full security review, each shipped as its own tracked phase rather than a single rewrite.
The frontend is plain JavaScript (no TypeScript), uses the Next.js App Router with a hand-rolled i18n route split instead of next-intl, and talks to a Statamic GraphQL CMS via Apollo Client. It deploys to Azure Static Web Apps, whose hybrid hosting mode doesn't support on-demand ISR revalidation or cached-image revalidation — a platform constraint independent of the app code.
Migrated the frontend's GraphQL integration from Statamic 4 to the upgraded Statamic 6 CMS — adapted the Link fieldtype's schema change (string to an object), fixed a pre-existing single-news-page locale bug, and added Apollo cache type policies to remove cache warnings.
Ran a security review and shipped 8 fixes: server-side reCAPTCHA verification for the fraud-awareness quiz, sanitization plus a URL-scheme allowlist for all CMS-authored HTML, a Content-Security-Policy and baseline security headers, and lint/audit gates added to the CI pipeline.
De-risked a Next.js 14→15 upgrade: ran the async-request-api codemod, fixed a build-breaking formatting issue, added CMS-data-shape guards a stricter build now requires, and verified the result with a 42 route×viewport visual-regression pass against the previous build.
Built out fraud-awareness quiz components and externalized the CMS base URL into a shared, fail-loud environment config.
Statamic 6 changed the Link fieldtype from a plain string to an object requiring a GraphQL sub-selection — a non-additive, schema-breaking change. The fix queries the nested URL field instead of the flat string, which only works against the new CMS; the old CMS rejects the sub-selection outright. Trade-off: this frontend branch can no longer run against both CMS versions — it must deploy in lockstep with the CMS cutover — accepted because straddling both schemas with conditional queries would have doubled the GraphQL surface for a migration that's a one-way door anyway.

The Content-Security-Policy has to allow unsafe-inline in script-src to keep an inline analytics snippet in the root layout working. Trade-off: unsafe-inline weakens the CSP's XSS protection for inline scripts specifically. Accepted for now over the larger effort of moving the snippet to a nonce/hash-based approach — flagged in the codebase itself as the upgrade path if the snippet is ever templated server-side.
The quiz and share-experience forms originally trusted the client-side reCAPTCHA widget alone. A server route now re-checks the token against the verification endpoint with a server-only secret before treating a submission as human-verified. Trade-off: an extra network round-trip on every submission, accepted because a client-only check can be bypassed entirely by calling the form-submission endpoint directly.

13+ components render CMS-authored rich text directly into the page (fraud tips, FAQs, leadership bios, embeds). A shared sanitization utility now runs every one through a sanitizer and constrains CMS-controlled link values to an http(s)/mailto/tel scheme allowlist. Trade-off: sanitization now runs on every render of CMS content instead of trusting the CMS's own editorial controls, accepted because the CMS accepts rich text from multiple editors and a single payload would otherwise execute directly in visitors' browsers.

The CMS-publish revalidation endpoint's secret check was hardened with a constant-time comparison instead of a plain string comparison, and the secret moved out of a committed literal into a server-only environment variable. The route stayed a GET even though a secret in a query string is more exposed in logs than a POST body, because an external CMS webhook may already call it by URL — switching to POST would have silently broken that integration with no way to verify from this frontend repo alone whether the CMS side would follow.
0
unit tests
0
E2E scenarios
checkout · setup-node (Node 22) · npm ci · npm run lint · npm audit --audit-level=high · build and deploy
No dedicated a11y test suite or jsx-a11y lint config exists in the repo; the framework's own core-web-vitals lint preset includes its built-in accessibility rules. The 94/100 score is a live Lighthouse measurement against production, not a static-analysis claim.
The live production site scores 96/100 on Lighthouse desktop performance and 94/100 accessibility, but only 63/100 on mobile performance, driven by a 5.4s LCP against the desktop run's 1.1s — consistent with the hero's video/image-heavy above-the-fold content under a throttled mobile profile. This is a frontend-only engagement with no analytics or business-metrics access, so no conversion, traffic, or engagement numbers can be reported here — only what Lighthouse measures directly.
Visit the live site ↗ (opens external site)This has been a multi-year, multi-engineer build rather than a solo project: git history runs back to September 2023 with several agency engineers contributing across nearly 300 commits. My own contribution covers the most recent phase — migrating the GraphQL layer to a new major CMS version, running a security review and shipping its fixes, and de-risking a Next.js version upgrade with a full visual-regression pass. The codebase's main structural gap is that it carries zero automated test coverage, before or after this work; the verification done here (build checks, live smoke tests, a one-off visual-regression run) was necessarily manual rather than a standing safety net, and that's worth flagging honestly rather than implying otherwise.