Files
sharper.ly/apps/web/README.md
T

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 eleven 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