@@ -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```
6371frontend/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
97116Rules:
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.
173193For 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.
200228See [ 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,
214252Request-local reuse is isolated to one incoming request. Server Component HMR caching
215253is disabled. A new server/document request rechecks eligibility; client history is
216254not 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