A bilingual (English/Arabic) Next.js 15 marketing site for Jeddah Prep and Grammar School (JPGS), a British-curriculum day school in Jeddah, Saudi Arabia — a ground-up rebuild of the school's existing WordPress site, now live at jpgs.org.

JPGS has run on WordPress at jpgs.org since at least the mid-2010s; this repo is a from-scratch Next.js rebuild covering the school's full public site — history back to 1967, academics by key stage, admissions, enrichment, wellbeing/pastoral care, staff directory, library and help-desk contacts, careers, and policy/FAQ pages. All of that copy lives in this codebase already: roughly 80 route-specific components under src/components/dynamic_components, assembled per route from typed content objects.
Every page renders through one App Router catch-all (src/app/[locale]/[...slug]/page.tsx) that looks a route slug up in a big content table and hands each entry to a ComponentMapper, which dynamically imports the named component. Bilingual EN/AR routing is wired via next-intl with an 'as-needed' locale prefix and dir={dir(locale)} on <html>, and an Apollo/GraphQL client is scaffolded (src/lib/client.ts) for a future CMS backend. The site is now live at jpgs.org; there is still no test suite or CI configuration in the repo.
ComponentMapper.tsx takes an array of { componenthandel } entries and resolves each one via dynamic(() => import(`@/components/dynamiccomponents/${component.component_handel}`)), falling back to a FallBackComponent on import failure. The trade-off: webpack can't statically analyze a template-string import path, so there's no shared-chunk optimization across these ~84 components and any typo or renamed file silently degrades to an empty fallback instead of a build error — worth it because it means a page's section order and composition live entirely in a plain data array, which is exactly the shape a future CMS integration needs.

next-intl is configured with localePrefix: 'as-needed' (English at '/', Arabic at '/ar/...') and the root layout sets <html dir={dir(locale)}>, which genuinely mirrors the header/nav layout on the /ar route. The trade-off: the message catalogs only carry ~30 UI-chrome strings (nav labels, form validation) per locale — every page's actual body copy in the [...slug] catch-all is hardcoded English with no Arabic variant — so today's Arabic route is a structurally-RTL shell over English content, not a translated site. A visible side effect: because the paragraph text isn't marked as Arabic script, the browser's own bidi algorithm still reorders trailing punctuation in that English copy when the document is dir="rtl".

StaffTabs.tsx keeps the active category/department selection in local React state rather than the URL, filtering an already-loaded staff array with Framer Motion transitions and a Swiper grid. The trade-off: no shareable/bookmarkable link to a specific department, in exchange for instant client-side filtering with zero round-trips, since the whole staff dataset is already inlined in the page payload. One concrete gap found while verifying this: each card's name and title (StaffTabs.tsx:260-261) are still hardcoded JSX literals rather than reading {teacher.name}/{teacher.role} from the mapped data, so every card shows the same name and title regardless of whose photo is displayed.

Apollo Client is fully wired for React Server Components (src/lib/client.ts, registerApolloClient), but the HttpLink's uri is left blank/commented out, and every page's actual content is a large inline typed object living directly inside page.tsx (the [...slug] catch-all alone is over 4,200 lines). The trade-off: this lets the design and layout ship and get reviewed well before any CMS or backend exists — at the cost of a manual, single-file content-editing workflow until someone points that idle HttpLink at a real GraphQL endpoint.
next.config.ts sets images: { unoptimized: true } despite also configuring avif/webp formats and responsive deviceSizes/imageSizes — meaning none of that resizing logic actually runs. The trade-off: simpler hosting (no image-optimization runtime dependency) in exchange for shipping the same full-resolution assets (public/assets is ~256MB across ~509 files) to every device. This is very likely a large part of why the live site's own PageSpeed Insights run flags ~15MB of achievable caching savings and a mobile LCP over 8s in the lab test — worth revisiting now that it's live.
src/app/[locale]/api/revalidate/route.ts compares a plain query-string secret against one static env var, then calls revalidatePath('/', 'layout') plus the requested path. The trade-off: it's the simplest possible "publish and go live" hook for a future CMS/webhook — no token rotation, rate-limiting, or scoped permissions — which is a reasonable bet for a low-stakes marketing site where a leaked token only lets someone force a cache refresh, not access data.
0
unit tests
0
E2E scenarios
eslint.config.mjs extends next/core-web-vitals (which bundles jsx-a11y) but then turns off label-has-associated-control, control-has-associated-label, click-events-have-key-events, no-autofocus, and no-static-element-interactions. A measured Lighthouse accessibility audit against the live production site (jpgs.org) scored 66/100 (desktop) and 68/100 (mobile), with concrete failing checks for button-name, color-contrast, heading-order, link-name, list structure, and touch target-size.
JPGS is now live at jpgs.org. PageSpeed Insights against the live site (checked 2026-07-07) gives two different pictures: lab (simulated) scores of 71 performance / 66 accessibility / 96 best-practices / 67 SEO on desktop, and 55 / 68 / 92 / 67 on mobile — the mobile lab run simulates a throttled low-end device, which is why its LCP reads 8.1s. Real-user field data (Chrome UX Report, latest 28 days) tells a better story on mobile — LCP 3.2s, INP 257ms, CLS 0.08 — but Google's own Core Web Vitals Assessment still calls both desktop and mobile a fail. PageSpeed's own diagnostics flag close to 22MB of total page weight and ~15MB of achievable caching savings, consistent with the unoptimized-images trade-off already flagged in D-02. No conversion or business-impact numbers are claimed here — only what PageSpeed measured.
Visit the live site ↗ (opens external site)This codebase is content-complete for a 55-year-old school's full public site — history, academics, admissions, wellbeing, enrichment, staff, library, careers — with a genuinely bilingual, RTL-ready routing layer and a component system shaped to slot a CMS in later without a rewrite. The honest gaps found while auditing it: the message catalogs (src/messages/en.json and ar.json) still carry leftover strings from what looks like a different, unrelated project — e-commerce terms like "TruckBlock" and "Wishlist" in both languages, suggesting this repo was bootstrapped from a prior codebase and never fully swept. The staff directory renders real per-teacher photos but a hardcoded name/title on every card (see A-03) — a scaffold that was never finished. There's no automated test coverage and no CI, and eslint.config.mjs disables several jsx-a11y rules outright, which lines up with the measured Lighthouse accessibility gaps (button/link names, color contrast, heading order, touch target size). And next.config.ts's images.unoptimized: true is very likely the single biggest lever behind the mobile performance gap PageSpeed reports against the live site — worth fixing now that it's actually live.