123 lines
5.8 KiB
Markdown
123 lines
5.8 KiB
Markdown
# `apps/web` — SHARP-ER front end
|
|||
|
|
|
||
|
|
React Router 8 in **framework mode** with `ssr: true`, React 19, Vite 8 and Tailwind CSS 4.
|
||
|
|
It renders the bilingual (EN / AR) SHARP-ER website and reads its content from the Strapi CMS
|
||
|
|
in `apps/cms`.
|
||
|
|
|
||
|
|
Setup, the database, Docker and everything else that spans both applications is in the
|
||
|
|
[repository README](../../README.md). This file covers the web application only.
|
||
|
|
|
||
|
|
## Run it
|
||
|
|
|
||
|
|
Always install from the repository root (`pnpm install`) — this is a pnpm workspace with a
|
||
|
|
hoisted `node_modules`.
|
||
|
|
|
||
|
|
```bash
|
||
|
|
pnpm --filter web dev # http://localhost:5173, HMR (or: pnpm dev:web)
|
||
|
|
pnpm --filter web build # build/client + build/server
|
||
|
|
pnpm --filter web start # react-router-serve, http://localhost:3000
|
||
|
|
pnpm --filter web typecheck # react-router typegen && tsc
|
||
|
|
```
|
||
|
|
|
||
|
|
The server reads three variables from its process environment, all optional in development:
|
||
|
|
`STRAPI_URL` (default `http://localhost:1337`), `STRAPI_TOKEN` and `SITE_URL` (unset means
|
||
|
|
the canonical URL is derived from the request).
|
||
|
|
|
||
|
|
## Layout
|
||
|
|
|
||
|
|
```
|
||
|
|
app/
|
||
|
|
├── root.tsx HTML shell: <html lang/dir>, canonical + hreflang, <Navigation>, <Footer>
|
||
|
|
├── routes.ts Route config: "/" redirect, the ":lang" layout, the ten sections
|
||
|
|
├── routes/
|
||
|
|
│ ├── *.tsx One module per page, serving BOTH locales
|
||
|
|
│ ├── _page-kit/ Section/hero/prose primitives, markdown renderer, CMS helpers
|
||
|
|
│ ├── _shared/ Listing helpers + listing UI (filters, tabs, pagination, empty states)
|
||
|
|
│ └── _events-opportunities/ Event and opportunity models, forms, .ics generation
|
||
|
|
├── components/ui/ Button · Input · Card · Navigation · Footer · LanguageSwitcher
|
||
|
|
├── styles/ Design tokens — see styles/README.md
|
||
|
|
├── i18n/ Locale config, paths, translator, formatters, messages/{en,ar}.json
|
||
|
|
└── lib/ strapi.ts · seo.ts · env.server.ts · route-slugs.ts · site-navigation.ts
|
||
|
|
public/
|
||
|
|
├── assets/ Logos, EU emblem, partner logos, icons
|
||
|
|
└── fonts/ Licensed woff2 files — NOT in git, see the root README
|
||
|
|
```
|
||
|
|
|
||
|
|
## URLs
|
||
|
|
|
||
|
|
```
|
||
|
|
/ 302 (Vary: Accept-Language) -> /en or /ar
|
||
|
|
/:lang home
|
||
|
|
/:lang/<section> about · what-we-do · activities-and-initiatives · events-calendar ·
|
||
|
|
applications-and-opportunities · news-and-updates · contact ·
|
||
|
|
resources-and-publications · knowledge-hub
|
||
|
|
/:lang/<section>/:slug detail pages (events, opportunities, resources, knowledge, news, activities)
|
||
|
|
/EN/about 301 -> /en/about
|
||
|
|
anything else localised 404
|
||
|
|
```
|
||
|
|
|
||
|
|
One route module serves both languages: `params.lang` is validated by the `:lang` layout, so
|
||
|
|
inside a page it is always `"en" | "ar"`. Add a section module at the path
|
||
|
|
`SECTION_MODULES` names in `app/routes.ts` and it is wired on the next build — Vite does not
|
||
|
|
re-evaluate that file when a module appears, so restart the dev server.
|
||
|
|
|
||
|
|
## Data
|
||
|
|
|
||
|
|
Content is fetched **in loaders, on the server**, through `~/lib/strapi`:
|
||
|
|
|
||
|
|
```ts
|
||
|
|
const { data } = await getMany<Story>("stories", { locale, sort: "publicationDate:desc" });
|
||
|
|
const story = requireEntity(await getBySlug<Story>("stories", params.slug, { locale }));
|
||
|
|
```
|
||
|
|
|
||
|
|
`getMany` · `getOne` · `getOneOrNull` · `getSingle` · `getBySlug` · `findFirst`, plus
|
||
|
|
`mediaUrl`, `requireEntity` and `notFound`. The client is server-only; `STRAPI_TOKEN` never
|
||
|
|
reaches the browser. Listing pages swallow CMS failures and fall back to the approved draft
|
||
|
|
copy, so the site stays up when Strapi is down — `requireEntity` is for genuine per-slug 404s.
|
||
|
|
|
||
|
|
SEO comes from `~/lib/seo` (`buildMeta` plus the JSON-LD builders). `root.tsx` already emits
|
||
|
|
the canonical link and the `en` / `ar` / `x-default` alternates for every route, so a page's
|
||
|
|
`meta` must **not** pass `alternates: true`; override the canonical with
|
||
|
|
`handle.canonicalPath` instead. In `meta`, read the root data with `readRootSeo(matches)` —
|
||
|
|
React Router 8 exposes `loaderData` on a match, not `data`.
|
||
|
|
|
||
|
|
## Styling
|
||
|
|
|
||
|
|
Two-tier design tokens: tier 1 is raw values (`app/styles/primitives.css`), tier 2 is meaning
|
||
|
|
(`semantic.css`, `typography.css`). **Components reference tier-2 tokens only** — no raw hex,
|
||
|
|
no tier-1 primitive, no raw font sizes. `:root` is the dark surface; `data-surface="light"`
|
||
|
|
flips a subtree and repaints itself.
|
||
|
|
|
||
|
|
Full reference: [`app/styles/README.md`](app/styles/README.md).
|
||
|
|
|
||
|
|
## Bilingual and RTL
|
||
|
|
|
||
|
|
Import from `~/i18n`: `useTranslation()` in components, `translate(locale, key)` in loaders,
|
||
|
|
`localizedPath` / `sectionPath` for links, `formatDate` / `formatNumber` for values. UI
|
||
|
|
strings live in `app/i18n/messages/en.json` and `ar.json` with identical, typechecked keys.
|
||
|
|
|
||
|
|
Lay out with **logical properties only** — `ps-` `pe-` `ms-` `me-` `start-` `end-`,
|
||
|
|
`margin-inline-start`, `text-align: start` — never `pl-` / `pr-` / `left` / `right`. Arabic
|
||
|
|
then mirrors for free. `rtl:` and `ltr:` variants exist for the rare case that cannot be
|
||
|
|
expressed logically.
|
||
|
|
|
||
|
|
## Docker
|
||
|
|
|
||
|
|
`Dockerfile` builds this application for production. Its build context is the **repository
|
||
|
|
root**, because the pnpm lockfile lives there:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
docker build -f apps/web/Dockerfile -t sharper-web . # from the repository root
|
||
|
|
docker compose up -d --build web # or through compose
|
||
|
|
```
|
||
|
|
|
||
|
|
It is a pnpm adaptation of the React Router template Dockerfile: install with
|
||
|
|
`--filter web...`, `react-router build`, then serve `build/server/index.js` with
|
||
|
|
`react-router-serve` on port 3000, with a health check on `/en`.
|
||
|
|
|
||
|
|
## Further reading
|
||
|
|
|
||
|
|
- [React Router docs](https://reactrouter.com/) — framework mode, loaders, `meta`, `handle`
|
||
|
|
- [`../../README.md`](../../README.md) — setting the whole project up, both paths
|
||
|
|
- [`../../CLAUDE.md`](../../CLAUDE.md) — the house rules in short form
|