Skip to content

Commit 3783461

Browse files
authored
Merge pull request #27 from HodeTech/development
feat(public): complete P02d-6 localized public renderer
2 parents 863f5c1 + 46ed971 commit 3783461

134 files changed

Lines changed: 18559 additions & 1420 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.claude/skills/add-frontend-route/SKILL.md‎

Lines changed: 111 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ SDK contract per
4141

4242
| Input | Required | Description |
4343
|-------|----------|-------------|
44-
| Path | Yes | URL path (`/courses/[slug]`, `/dashboard/users`, `/lesson/[id]`). |
44+
| Path | Yes | Public URL `/{locale}/courses/{slug}`; future authenticated paths follow their owning phase. |
4545
| Route group | Yes | `(public)` / `(studio)` / `(portal)`. |
4646
| Auth | Yes | Anonymous (public) / authenticated tenant / org-scoped. |
4747
| Permission key | If guarded | `{module}.{resource}.{action}` from the closed set. |
@@ -59,6 +59,14 @@ SDK contract per
5959

6060
### Step 2: Create the route folder
6161

62+
The shipped public tree is `(public)/[locale]/courses/`, with course detail at
63+
`[slug]/page.tsx` and lesson detail at `[slug]/lessons/[lessonSlug]/page.tsx`.
64+
`courses` and `lessons` are fixed section segments. The only localized status
65+
namespace is `[locale]/status/not-found`. A new public segment also needs an
66+
explicit update to the closed `contentPath` / `publicEntry` admission policy and
67+
its tests; adding a Next folder alone leaves it refused. Phase 06 owns broader
68+
localized section names.
69+
6270
```
6371
frontend/apps/web/src/app/
6472
(studio)/
@@ -75,24 +83,35 @@ unless the screen genuinely needs one.
7583
### Step 3: Server Component shell
7684

7785
```tsx
78-
// page.tsx (Server Component by default)
79-
import { createConfiguredPublicClient } from '@/server/configured-public-client';
80-
81-
// The configured server caller supplies this transport; never a tenant-ID option.
82-
export function loadCourses(envelope: string | null) {
83-
const client = createConfiguredPublicClient(envelope);
84-
if (!client) throw new Error('Invalid public ingress');
85-
return client.getCourses(); // Locale comes from the authenticated route.
86+
// app/(public)/[locale]/courses/page.tsx
87+
import { notFound } from 'next/navigation';
88+
import { PublicCatalog } from '@/components/public/catalog';
89+
import { PublicState } from '@/components/public/state';
90+
import { requirePublicResource } from '@/server/public-resource';
91+
import { getPublicUi } from '@/server/public-ui';
92+
93+
export default async function CoursesPage() {
94+
const resource = await requirePublicResource();
95+
const ui = await getPublicUi();
96+
if (resource.kind === 'failure') {
97+
return <PublicState state={resource.state}
98+
recoveryPath={resource.request.route.path}
99+
locale={ui.locale} direction={ui.direction} t={ui.t} />;
100+
}
101+
if (resource.kind !== 'catalog') notFound();
102+
return <PublicCatalog resource={resource} ui={ui} />;
86103
}
87104
```
88105

89106
> **P02d-4 delivered.** `@learnstack/sdk/server` exports an injected
90107
> `createServerSdk(transport)` with four typed public GET wrappers; no global `sdk`
91-
> object, tenant-ID option or module namespace exists. The example is a loader,
92-
> not a complete route. P02d-5/G35 delivers the configured trusted transport in
108+
> object, tenant-ID option or module namespace exists. P02d-5/G35 delivers the
109+
> configured trusted transport in
93110
> [Phase 02d's decision register](../../../docs/roadmap/phase-02d-walking-skeleton.md#the-decision-register);
94-
> that caller precedes P02d-6 public page consumers. Read the verified envelope
95-
> from request-local `headers()`; never create or expose a provenance stamp in a page.
111+
> that caller precedes P02d-6 public page consumers. Pages use the shared resource
112+
> loader; they do not read envelope headers or create another bootstrap/client.
113+
> Keep loader helpers outside `page.tsx`; Next page exports are restricted to
114+
> supported route exports such as the default component and `generateMetadata`.
96115
97116
Rules:
98117

@@ -170,6 +189,7 @@ is authoritative.
170189

171190
### Step 6: Feature gating (entitlement-aware UI)
172191

192+
The hook/package below is a future feature-UI sketch, not a shipped SDK export.
173193
For features gated by plan-projected `FeatureKey`:
174194

175195
```tsx
@@ -187,23 +207,41 @@ Hide, don't disable. The hook reads the entitlement projection. See
187207

188208
### Step 7: Localisation
189209

210+
**P02d-6 G39 foundation delivered — Step 1, 2026-10-10.**
211+
[ADR-0027](../../../docs/decisions/0027-frontend-i18n.md) selects exact `next-intl`
212+
4.14.9, now installed with complete English/Turkish catalogues. The async Server
213+
Component pattern is:
214+
190215
```tsx
191-
import { useTranslations } from "next-intl"; // or react-intl per the i18n ADR
216+
import { getPublicUi } from "@/server/public-ui";
192217

193-
export default function CoursesPage() {
194-
const t = useTranslations("courses");
195-
return <h1>{t("title")}</h1>;
218+
export default async function CoursesPage() {
219+
const { t } = await getPublicUi();
220+
return <h1>{t("catalog.title")}</h1>;
196221
}
197222
```
198223

199-
Translation keys live under `frontend/apps/web/src/i18n/<locale>/courses.json`.
224+
Messages live in `frontend/apps/web/src/i18n/messages/<locale>/<namespace>.json`;
225+
P02d-6 supplies complete `en/public.json` and `tr/public.json`. General UI keys
226+
are dotted feature identifiers, distinct from backend `lockey_*` wire keys. The
227+
web app owns closed page-outcome mappings; the SDK supplies no translations.
200228
See [add-i18n-key](../add-i18n-key/SKILL.md).
201229

202-
> **Open in Phase 02d.** No i18n library is installed and no catalogue exists. Whether
203-
> ADR-0027 picks the library in Phase 02d, and where the one UI string catalogue
204-
> lives — the corpus names three paths — are G39 in
205-
> [Phase 02d's decision register](../../../docs/roadmap/phase-02d-walking-skeleton.md#the-decision-register);
206-
> the pass that closes it edits this step and add-i18n-key.
230+
`src/i18n/request.ts` uses the same server-only, request-cached verified admission
231+
loader as document/layout/page consumers. It re-verifies the ingress envelope,
232+
reads the canonical locale from the signed target and checks live enabled-locale
233+
membership. The loader neither imports next-intl nor reads messages. No i18n
234+
routing middleware, preference cookie, locale header or callsite override supplies
235+
authority. Only whole-catalogue fallback is allowed for unauthored UI languages;
236+
missing used keys in supported catalogues fail validation. Preserve exact content
237+
locale, document language and actual resolved-label language.
238+
239+
The guarded frontend suite supplies ICU, argument and callsite checks. No
240+
`lint:i18n` command or screenshot/axe tooling exists. The
241+
[Step 1 delivery record](../../../docs/roadmap/phase-02d-walking-skeleton.md#p02d-6-step-1-localization-and-document-foundation)
242+
owns foundation validation; the
243+
[packet closeout](../../../docs/roadmap/phase-02d-walking-skeleton.md#p02d-6-packet-closeout-2026-10-10)
244+
records the delivered product proof and scoped manual accessibility checks.
207245

208246
### Step 8: Public-site SSR caching
209247

@@ -214,30 +252,69 @@ public rendering and no-store API transport; no positive `revalidate`, ISR,
214252
Request-local reuse is isolated to one incoming request. Server Component HMR caching
215253
is disabled. A new server/document request rechecks eligibility; client history is
216254
not a revocation guarantee. Use the configured server caller; never derive tenancy
217-
from a page header or add hop options to the injected SDK. P6 owns page consumers.
255+
from a page header or add hop options to the injected SDK.
256+
257+
**Accepted P02d-6 G40 — 2026-10-09; foundation delivered in Step 1.** Use ordinary
258+
same-host relative anchors for public navigation and pagination, without automatic
259+
prefetch or reliance on retained client Router Cache. Request-local metadata,
260+
layout and page share verified admission and resource loaders; each page honors
261+
their result before emitting a shell. No loading boundary may flush before
262+
redirect admission.
263+
264+
Parse owned pagination values only from the verified raw signed target, never
265+
observed Next `searchParams`. Catalog uses `cursor`/`limit`; outline uses
266+
`lessonCursor`/`lessonLimit`, default 20 and API bounds. Refuse duplicate, empty,
267+
malformed or oversized owned values; never decode opaque cursors or fabricate a
268+
previous cursor. Paginated metadata is noindex with a cursor-free canonical.
269+
Canonical/alternate URLs use the verified live host and eligible API slugs.
218270

219271
### Step 9: Loading + error boundaries
220272

221-
Every route ships its own:
222-
223-
- `loading.tsx` — skeleton shell, not a blank page. No "loading…" spinners for
224-
expected-fast resources (<250 ms).
225-
- `error.tsx` — graceful boundary; 404 page renders the tenant's brand if a
226-
tenant was resolved.
273+
Each route retains `loading.tsx` and a graceful `error.tsx`; P02d-6's accepted
274+
status composition is distinct from the framework's thrown `notFound()` behavior:
275+
276+
- A missing/hidden content resource returns local HTTP 307 to the same host's
277+
`/{locale}/status/not-found`. The browser URL changes; the original response is
278+
not a direct 404.
279+
- Middleware admits that fixed status namespace through the same live host and
280+
locale checks, then supplies HTTP 404. Its ordinary server-rendered document has
281+
localized language/direction, safe theme/chrome, noindex metadata and a catalog
282+
recovery link. It never queries Education or echoes the original slug/query.
283+
- Unknown-host/provenance refusals remain direct masked responses. Fresh status
284+
bootstrap failure retains neutral 404/429/503; fallback UI cannot invent tenant
285+
admission. HEAD is bodyless throughout.
286+
- Known content-call failures are controlled translated HTTP 200 noindex states:
287+
invalid cursor with reset link, retry-later for 429, unavailable for transport,
288+
invalid responses or unavailable API. They do not claim HTTP 400/429/503.
289+
Closed outcome mapping never exposes backend keys, titles, field errors or
290+
parameters as lookup identifiers or visible copy.
291+
- Unexpected framework errors keep pre-stream 500 / post-stream 200 behavior;
292+
`error.tsx` cannot set arbitrary status.
293+
294+
ADR-0055's implemented single bootstrap costs two API calls per completed product
295+
document, one per fixed status/scaffold document and three per followed missing
296+
document. Metadata/layout/page/UI share the admitted snapshot without another
297+
bootstrap or cross-request cache. HEAD/RSC/prefetch and Flight fallback require
298+
separately proven counts. The
299+
[Step 3 record](../../../docs/roadmap/phase-02d-walking-skeleton.md#adr-0055-step-3--production-admission-proof-and-closeout)
300+
records passing production replacement proofs, completed reviews and verified fixes.
227301

228302
### Step 10: Tests
229303

230-
- Component tests (`frontend/apps/web/src/app/(studio)/dashboard/users/page.test.tsx`)
231-
with Testing Library, per
304+
- Synchronous view/mapping tests use Vitest/Testing Library; async public pages use
305+
the real production HTML/RSC fixture, per
232306
[Testing Standards § Frontend Test Types](../../../docs/standards/06-testing.md#frontend-test-types).
233307
Automated `axe-core` runs through Playwright, owned by
234308
[Phase 06](../../../docs/roadmap/phase-06-renderer-admin-studio.md) per
235309
[Testing Standards § End-to-End Tests](../../../docs/standards/06-testing.md#end-to-end-tests);
236310
the manual keyboard and contrast checks
237311
[Accessibility Standards § Tooling](../../../docs/standards/16-accessibility.md#tooling)
238312
and [§ Testing](../../../docs/standards/16-accessibility.md#testing) require are
239-
recorded in the PR description. The phase that ships a route names its test set in
240-
its decision register.
313+
recorded in the PR description. P02d-6 also requires actual manual screen-reader,
314+
keyboard, focus, 320 CSS px reflow/zoom, long-string and contrast evidence on both
315+
hosts/locales. If unavailable, evidence stays pending and completion is not
316+
claimed. HTTP/RSC assertions are not browser or assistive-technology proof.
317+
P02d-7 owns browser/demo and Lighthouse; Phase 11 owns web-vitals telemetry.
241318
- Lighthouse budget check on representative public routes — CI's `lighthouse budget`
242319
job remains deferred until P02d-7/G44/G45 after P6 pages; judge by reading until
243320
that harness is implemented. Its remaining details are in the

0 commit comments

Comments
 (0)