Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
848b333
docs(roadmap): close P02d-5 after PR merge
cemililik Oct 9, 2026
64eefe8
docs(roadmap): prepare P02d-6 renderer decisions
cemililik Oct 9, 2026
2d821c4
docs(decisions): reconcile UI localization review findings
cemililik Oct 9, 2026
c14ecf2
docs(decisions): accept P02d-6 renderer and UI localization
cemililik Oct 9, 2026
568ecc2
feat(public): add localized document and status foundation
cemililik Oct 9, 2026
73b9d53
docs(roadmap): align P02d-6 foundation delivery status
cemililik Oct 9, 2026
785c8c5
docs(public): close P02d-6 foundation review rounds
cemililik Oct 9, 2026
917f9f8
feat(public): render catalog and course pages
cemililik Oct 9, 2026
86f8ea1
docs(roadmap): record P02d-6 catalog review round one
cemililik Oct 9, 2026
c250677
fix(public): include eligible metadata self references
cemililik Oct 9, 2026
0be38e8
feat(public): render ordered lesson presentation
cemililik Oct 9, 2026
3ac4ee1
fix(public): align failure recovery with its catalog label
cemililik Oct 9, 2026
593d3e1
fix(public): reserve cursor recovery for paginated reads
cemililik Oct 9, 2026
e2b8de4
test(public): complete the defensive validation error fixture
cemililik Oct 9, 2026
e073075
test(public): prove concurrent product isolation and recovery
cemililik Oct 9, 2026
b86e20d
test(public): cover private problem types in product containment
cemililik Oct 9, 2026
180dde0
test(public): close product proof review gaps
cemililik Oct 9, 2026
5d7aafc
docs(roadmap): record manual P02d-6 browser observations
cemililik Oct 9, 2026
27a02ba
docs(roadmap): keep P02d-6 screen-reader closeout pending
cemililik Oct 9, 2026
7d98228
docs: close P02d-6 manual accessibility and delivery
cemililik Oct 9, 2026
2b6dbb4
docs(standards): remove stale status counts after P6 closeout
cemililik Oct 9, 2026
47f9c12
docs(roadmap): record final P02d-6 closeout reviews
cemililik Oct 10, 2026
e871e88
fix(public): close renderer review proof gaps
cemililik Oct 10, 2026
ed6f81e
docs(roadmap): record P6 remediation review approvals
cemililik Oct 10, 2026
cc626e8
fix(public): strengthen P6 proof and guidance boundaries
cemililik Oct 10, 2026
69b382a
fix(public): close retention and attribution proof gaps
cemililik Oct 10, 2026
bdb88bb
fix(public): trace member factories and selected captures
cemililik Oct 10, 2026
ca03942
docs(decisions): propose middleware-owned public admission
cemililik Oct 10, 2026
75c3e61
fix(public): project supported Open Graph locales
cemililik Oct 10, 2026
f16d937
test(public): require complete Open Graph locale output
cemililik Oct 10, 2026
3f6f37f
fix(web): patch Next and close unused image optimization
cemililik Oct 10, 2026
05a4313
docs(roadmap): record first security remediation reviews
cemililik Oct 10, 2026
46bc8a4
docs(roadmap): close approved security remediation reviews
cemililik Oct 10, 2026
2bf9d57
docs(decisions): reconcile public admission draft review
cemililik Oct 10, 2026
0761d84
chore(web): upgrade the approved test toolchain
cemililik Oct 10, 2026
a97f6ee
docs(roadmap): record test toolchain first reviews
cemililik Oct 10, 2026
22a9775
docs(roadmap): close test toolchain review rounds
cemililik Oct 10, 2026
2deb7b5
docs(decisions): record isolated admission feasibility
cemililik Oct 10, 2026
5b5f5a9
docs(decisions): distinguish late admission refusal
cemililik Oct 10, 2026
b29f751
docs(roadmap): close isolated admission feasibility review
cemililik Oct 10, 2026
6e05fd8
docs(decisions): accept single-bootstrap public admission
cemililik Oct 10, 2026
166ffb3
feat(public): own admission in the native request lifetime
cemililik Oct 10, 2026
af34e29
fix(public): release completed admission lifetimes
cemililik Oct 10, 2026
c9a3c93
fix(public): close native shutdown and installer alias gaps
cemililik Oct 10, 2026
578dcd8
feat(public): reuse the native middleware admission snapshot
cemililik Oct 10, 2026
7a2b4b2
docs(roadmap): record single-bootstrap step two reviews
cemililik Oct 10, 2026
37ec51e
test(public): prove single-bootstrap production admission
cemililik Oct 10, 2026
9f785fc
test(public): isolate healthy admission failure stages
cemililik Oct 10, 2026
b8d5199
test(public): await native events before asserting release
cemililik Oct 10, 2026
100534f
test(public): detect premature completion of native proof
cemililik Oct 10, 2026
7797842
docs(roadmap): close reviewed single-bootstrap admission delivery
cemililik Oct 10, 2026
ef05ab0
test(public): isolate renderer log capture from other hosts
cemililik Oct 10, 2026
d1cc011
docs(roadmap): record verified admission capture fix reviews
cemililik Oct 10, 2026
1496269
fix(public): align listener port and prove scaffold isolation
cemililik Oct 10, 2026
b073f35
docs(roadmap): record scaffold isolation review evidence
cemililik Oct 10, 2026
7ea14c5
fix(public): preserve ingress proof instrumentation after port refactor
cemililik Oct 10, 2026
dd5ec95
docs(roadmap): close ingress instrumentation fix reviews
cemililik Oct 10, 2026
46ed971
docs(standards): Mark ADR-0055 Step 3 review rounds complete
coderabbitai[bot] Oct 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 111 additions & 34 deletions .claude/skills/add-frontend-route/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ SDK contract per

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

### Step 2: Create the route folder

The shipped public tree is `(public)/[locale]/courses/`, with course detail at
`[slug]/page.tsx` and lesson detail at `[slug]/lessons/[lessonSlug]/page.tsx`.
`courses` and `lessons` are fixed section segments. The only localized status
namespace is `[locale]/status/not-found`. A new public segment also needs an
explicit update to the closed `contentPath` / `publicEntry` admission policy and
its tests; adding a Next folder alone leaves it refused. Phase 06 owns broader
localized section names.

```
frontend/apps/web/src/app/
(studio)/
Expand All @@ -75,24 +83,35 @@ unless the screen genuinely needs one.
### Step 3: Server Component shell

```tsx
// page.tsx (Server Component by default)
import { createConfiguredPublicClient } from '@/server/configured-public-client';

// The configured server caller supplies this transport; never a tenant-ID option.
export function loadCourses(envelope: string | null) {
const client = createConfiguredPublicClient(envelope);
if (!client) throw new Error('Invalid public ingress');
return client.getCourses(); // Locale comes from the authenticated route.
// app/(public)/[locale]/courses/page.tsx
import { notFound } from 'next/navigation';
import { PublicCatalog } from '@/components/public/catalog';
import { PublicState } from '@/components/public/state';
import { requirePublicResource } from '@/server/public-resource';
import { getPublicUi } from '@/server/public-ui';

export default async function CoursesPage() {
const resource = await requirePublicResource();
const ui = await getPublicUi();
if (resource.kind === 'failure') {
return <PublicState state={resource.state}
recoveryPath={resource.request.route.path}
locale={ui.locale} direction={ui.direction} t={ui.t} />;
}
if (resource.kind !== 'catalog') notFound();
return <PublicCatalog resource={resource} ui={ui} />;
}
```

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

Rules:

Expand Down Expand Up @@ -170,6 +189,7 @@ is authoritative.

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

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

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

### Step 7: Localisation

**P02d-6 G39 foundation delivered — Step 1, 2026-10-10.**
[ADR-0027](../../../docs/decisions/0027-frontend-i18n.md) selects exact `next-intl`
4.14.9, now installed with complete English/Turkish catalogues. The async Server
Component pattern is:

```tsx
import { useTranslations } from "next-intl"; // or react-intl per the i18n ADR
import { getPublicUi } from "@/server/public-ui";

export default function CoursesPage() {
const t = useTranslations("courses");
return <h1>{t("title")}</h1>;
export default async function CoursesPage() {
const { t } = await getPublicUi();
return <h1>{t("catalog.title")}</h1>;
}
```

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

> **Open in Phase 02d.** No i18n library is installed and no catalogue exists. Whether
> ADR-0027 picks the library in Phase 02d, and where the one UI string catalogue
> lives — the corpus names three paths — are G39 in
> [Phase 02d's decision register](../../../docs/roadmap/phase-02d-walking-skeleton.md#the-decision-register);
> the pass that closes it edits this step and add-i18n-key.
`src/i18n/request.ts` uses the same server-only, request-cached verified admission
loader as document/layout/page consumers. It re-verifies the ingress envelope,
reads the canonical locale from the signed target and checks live enabled-locale
membership. The loader neither imports next-intl nor reads messages. No i18n
routing middleware, preference cookie, locale header or callsite override supplies
authority. Only whole-catalogue fallback is allowed for unauthored UI languages;
missing used keys in supported catalogues fail validation. Preserve exact content
locale, document language and actual resolved-label language.

The guarded frontend suite supplies ICU, argument and callsite checks. No
`lint:i18n` command or screenshot/axe tooling exists. The
[Step 1 delivery record](../../../docs/roadmap/phase-02d-walking-skeleton.md#p02d-6-step-1-localization-and-document-foundation)
owns foundation validation; the
[packet closeout](../../../docs/roadmap/phase-02d-walking-skeleton.md#p02d-6-packet-closeout-2026-10-10)
records the delivered product proof and scoped manual accessibility checks.

### Step 8: Public-site SSR caching

Expand All @@ -214,30 +252,69 @@ public rendering and no-store API transport; no positive `revalidate`, ISR,
Request-local reuse is isolated to one incoming request. Server Component HMR caching
is disabled. A new server/document request rechecks eligibility; client history is
not a revocation guarantee. Use the configured server caller; never derive tenancy
from a page header or add hop options to the injected SDK. P6 owns page consumers.
from a page header or add hop options to the injected SDK.

**Accepted P02d-6 G40 — 2026-10-09; foundation delivered in Step 1.** Use ordinary
same-host relative anchors for public navigation and pagination, without automatic
prefetch or reliance on retained client Router Cache. Request-local metadata,
layout and page share verified admission and resource loaders; each page honors
their result before emitting a shell. No loading boundary may flush before
redirect admission.

Parse owned pagination values only from the verified raw signed target, never
observed Next `searchParams`. Catalog uses `cursor`/`limit`; outline uses
`lessonCursor`/`lessonLimit`, default 20 and API bounds. Refuse duplicate, empty,
malformed or oversized owned values; never decode opaque cursors or fabricate a
previous cursor. Paginated metadata is noindex with a cursor-free canonical.
Canonical/alternate URLs use the verified live host and eligible API slugs.

### Step 9: Loading + error boundaries

Every route ships its own:

- `loading.tsx` — skeleton shell, not a blank page. No "loading…" spinners for
expected-fast resources (<250 ms).
- `error.tsx` — graceful boundary; 404 page renders the tenant's brand if a
tenant was resolved.
Each route retains `loading.tsx` and a graceful `error.tsx`; P02d-6's accepted
status composition is distinct from the framework's thrown `notFound()` behavior:

- A missing/hidden content resource returns local HTTP 307 to the same host's
`/{locale}/status/not-found`. The browser URL changes; the original response is
not a direct 404.
- Middleware admits that fixed status namespace through the same live host and
locale checks, then supplies HTTP 404. Its ordinary server-rendered document has
localized language/direction, safe theme/chrome, noindex metadata and a catalog
recovery link. It never queries Education or echoes the original slug/query.
- Unknown-host/provenance refusals remain direct masked responses. Fresh status
bootstrap failure retains neutral 404/429/503; fallback UI cannot invent tenant
admission. HEAD is bodyless throughout.
- Known content-call failures are controlled translated HTTP 200 noindex states:
invalid cursor with reset link, retry-later for 429, unavailable for transport,
invalid responses or unavailable API. They do not claim HTTP 400/429/503.
Closed outcome mapping never exposes backend keys, titles, field errors or
parameters as lookup identifiers or visible copy.
- Unexpected framework errors keep pre-stream 500 / post-stream 200 behavior;
`error.tsx` cannot set arbitrary status.

ADR-0055's implemented single bootstrap costs two API calls per completed product
document, one per fixed status/scaffold document and three per followed missing
document. Metadata/layout/page/UI share the admitted snapshot without another
bootstrap or cross-request cache. HEAD/RSC/prefetch and Flight fallback require
separately proven counts. The
[Step 3 record](../../../docs/roadmap/phase-02d-walking-skeleton.md#adr-0055-step-3--production-admission-proof-and-closeout)
records passing production replacement proofs, completed reviews and verified fixes.

### Step 10: Tests

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