Runtime
Astro 6 + Starlight 0.39, output: 'server', @astrojs/node standalone — every doc request can read cookies and redirect.
This page is the narrative architecture guide for the DIH Knowledge Base PoC: Astro Starlight on SSR, Keycloak OIDC, product roles in the JWT, and what we deliberately do not ship compared to docs.dih.telekom.com.
Runtime
Astro 6 + Starlight 0.39, output: 'server', @astrojs/node standalone — every doc request can read cookies and redirect.
Identity
Keycloak authorization code + PKCE; access JWT in an HttpOnly cookie; logout via id_token_hint.
Authorization
Frontmatter-driven: requiresAuth, requiredRoles (WIKIJS_* groups) — three enforcement layers.
Locales
/en/ and /de/ content trees; / redirects to /en/ — aligned with production URL shape.
Every documentation URL passes through Astro middleware, then Starlight route middleware, then our custom page frame (header, sidebar, content or upsell).
flowchart TB subgraph Browser["Reader browser"] REQ["GET /en/products/..."] end subgraph Layer1["Layer 1 — src/middleware.ts"] JWT["Read dih_access_token cookie"] REDIR["Redirect to /auth/login?"] ACC["Set locals.pageAccess"] end subgraph Layer2["Layer 2 — src/route-middleware.ts"] SB["Filter sidebar by roles"] end subgraph Layer3["Layer 3 — AuthPageFrame.astro"] UP["UpsellScreen"] MDX["MDX page body"] end REQ --> JWT JWT -->|invalid / missing + requiresAuth| REDIR JWT --> ACC ACC --> SB SB -->|upsell| UP SB -->|allowed / public| MDX
/en/… or /de/…).pageAccess to public, allowed, or upsell.| Layer | Choice | Role |
|---|---|---|
| Framework | Astro 6 | SSR, content collections, API routes |
| Docs UI | Starlight 0.39 | Sidebar, MDX, i18n, component overrides |
| Runtime | @astrojs/node (standalone) | node ./dist/server/entry.mjs in K8s |
| Auth | Keycloak + jose | OIDC, JWKS validation |
| Search | public/search-index.json | Built at dev/build; filtered in header (not Pagefind) |
| Styling | TeleNeo Web + custom CSS | Telekom chrome; no ODS React runtime |
Starlight Pagefind expects prerendered HTML. This PoC sets prerender: false and pagefind: false so that:
Trade-off: you host a Node process (Docker image in-repo) instead of a pure static bucket.
| Area | Path |
|---|---|
| Astro config | astro.config.mjs — locales, sidebar, component overrides |
| Content schema | src/content.config.ts — requiresAuth, requiredRoles, upsell fields |
| Access logic | src/lib/access.ts — getPageAccess, sidebar visibility |
| OIDC | src/lib/keycloak.ts, src/pages/auth/*.ts |
| JWT | src/lib/jwt.ts — verify access token, warm JWKS |
| Dev OAuth fix | src/vite/auth-dev-middleware.ts — Sec-Fetch relax for /auth/* only |
Sign-in is OIDC authorization code with PKCE. Auth routes live at the site root (/auth/…) so the same flow works from /en/ and /de/.
sequenceDiagram participant U as "Browser" participant D as "Docs (Astro SSR)" participant K as "Keycloak (iam.dev.dih-cloud.com)" U->>D: GET /en/guides/platform-overview/ D->>U: 302 /auth/login?returnTo=... U->>D: GET /auth/login Note over D: Store PKCE verifier, state, returnTo D->>U: 303 authorize URL (PKCE) U->>K: User signs in (SSO) K->>U: 302 /auth/callback?code=... U->>D: GET /auth/callback D->>K: POST token (code + verifier) K-->>D: access_token, id_token Note over D: Set dih_access_token + dih_id_token cookies D->>U: 302 returnTo (original page) U->>D: GET original page (JWT in cookie)
sequenceDiagram participant U as "Browser" participant D as "Docs" participant K as "Keycloak" U->>D: GET /auth/logout Note over D: Clear session cookies D->>U: 302 Keycloak end-session Note over D,K: id_token_hint + post_logout_redirect_uri K->>U: End SSO session K->>U: 302 http://localhost:4321/en/
GET /auth/logout.dih_access_token (and related) cookies.id_token_hint from dih_id_token.KEYCLOAK_POST_LOGOUT_REDIRECT_URI (default http://localhost:4321/en/ — must be registered on the client).| Route | Purpose |
|---|---|
/auth/login | Start OIDC; stores PKCE verifier, state, returnTo |
/auth/callback | Exchange code; set session cookies |
/auth/logout | Clear cookies; Keycloak end-session |
Session cookie — access JWT (SESSION_COOKIE_NAME, default dih_access_token).
Logout cookie — id_token (dih_id_token) for id_token_hint.
Roles are resolved from ROLE_CLAIM with fallbacks (groups, realm_access.roles, …). Mapping table: Access matrix.
KEYCLOAK_ISSUER=https://iam.dev.dih-cloud.com/realms/dihKEYCLOAK_CLIENT_ID=astro-starlightKEYCLOAK_CLIENT_SECRET=***KEYCLOAK_REDIRECT_URI=http://localhost:4321/auth/callbackAPP_BASE_URL=http://localhost:4321KEYCLOAK_POST_LOGOUT_REDIRECT_URI=http://localhost:4321/en/{ "sub": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "name": "Pat Evaluator", "groups": [ "WIKIJS_CONNECT_CUSTOMER" ], "azp": "astro-starlight"}Access is declarative in frontmatter — no separate permissions database in the PoC.
flowchart TD
A["Incoming request"] --> B{"Public page?<br/>no requiresAuth / roles"}
B -->|yes| Z["Render MDX content"]
B -->|no| C{"Valid JWT?"}
C -->|no| L["302 → /auth/login"]
C -->|yes| D{"requiredRoles<br/>in token?"}
D -->|yes| Z
D -->|no| U["UpsellScreen<br/>(URL unchanged)"] File: src/middleware.ts
/_astro/, /auth/, /health, static files with extensionsverifyAccessToken → Astro.locals.userpathnameToDocId → content entry for /en/… and /de/…requiresAuth without user → redirect to login with returnToAstro.locals.pageAccess via toStarlightPageAccessexport function getPageAccess( data: DocAccessFields, user?: User,): PageAccessResult { if (!pageNeedsAuth(data)) return 'public'; if (!user) return 'auth_required'; if (data.requiredRoles?.length && !userHasRequiredRole(data, user)) { return 'upsell'; } return 'allowed';}File: src/route-middleware.ts
docs collection entrieshref → entryfilterSidebar removes links the user cannot opentranslateSidebarLabelsSame visibility rules as header search (src/lib/search-access.ts).
File: src/components/AuthPageFrame.astro
When pageAccess === 'upsell' and a Starlight entry exists, render UpsellScreen instead of the default slot — user sees title, required products, CTA, but not the MDX body.
title: About DIHdescription: Platform introduction# No requiresAuth / requiredRolestitle: Platform overviewrequiresAuth: truetitle: Certificate managementrequiresAuth: truerequiredRoles: - WIKIJS_CONNECT_CUSTOMERupsellTitle: Connect & Integrate documentationupsellCtaLabel: Contact salesupsellCtaUrl: mailto:DIH_Sales@telekom.deProduction uses /en and /de without bare localized content at /. This PoC mirrors that.
flowchart LR ROOT["GET /"] --> EN["/en/ home"] EN --> ENC["content/docs/en/"] DE["/de/ home"] --> DEC["content/docs/de/"] ENC -.->|fallback| DEC
| Locale | Content folder | Example URL |
|---|---|---|
| English (default) | src/content/docs/en/ | /en/poc/architecture/ |
| Deutsch | src/content/docs/de/ | /de/poc/architecture/ |
defaultLocale: 'en' and redirects: { '/': '/en/' } in astro.config.mjssrc/lib/ui-copy.ts · German sidebar: src/lib/sidebar-labels.tsWe override Starlight primitives in astro.config.mjs instead of forking the theme.
| Override | File | Behaviour |
|---|---|---|
PageFrame | AuthPageFrame.astro | Upsell shell + DIH footer |
Header | Header.astro | Telekom T, brand, nav, DocsSearch, login, language |
Hero | Hero.astro + DihLandingHero.astro | Landing imagery on /en/ and /de/ |
Sidebar | Sidebar.astro | Mobile footer in drawer |
ThemeProvider | ThemeProvider.astro | Light-only (no dark toggle) |
Global flags: prerender: false, pagefind: false, routeMiddleware: './src/route-middleware.ts'.
Authoring samples
Markdown, Tabs, Steps, Expressive Code, Mermaid, API embed, downloads — under Documentation capabilities.
Custom Astro
MermaidDiagram, WorkshopEmbed, DocDownload, UpsellScreen — see Custom components.
flowchart LR BUILD["npm run dev / build"] --> IDX["generate-search-index.mjs"] IDX --> JSON["public/search-index.json"] JSON --> UI["DocsSearch in Header"] UI --> FILT["search-access.ts filters by roles"]
Starlight Pagefind is off because it conflicts with full SSR. Instead:
scripts/generate-search-index.mjs walks the docs collection and writes titles, descriptions, URLs, requiresAuth, requiredRoles.src/integrations/search-index.ts runs the script on dev and build.DocsSearch loads the JSON and hides hits the current user cannot access (mirrors sidebar rules).src/ content/docs/en/ # English MDX/Markdown content/docs/de/ # German (partial) components/ # Header, Hero, Upsell, MermaidDiagram, … lib/ # access, keycloak, jwt, paths, locale, search-access middleware.ts # Layer 1 route-middleware.ts # Layer 2 pages/auth/ # login.ts, callback.ts, logout.tspublic/ specs/ # OpenAPI 3 + Swagger 2.0 for Swagger UI embed downloads/ # CSV templates, Postman collection search-index.json # generated — do not hand-editExtended schema in src/content.config.ts: requiresAuth, requiredRoles, upsellTitle, upsellCtaLabel, upsellCtaUrl.
flowchart TB subgraph CI["GitLab CI / local"] GIT["git push"] BUILD["docker build"] end subgraph K8s["Kubernetes (target)"] POD["Pod: Node 22"] POD --> ENTRY["node dist/server/entry.mjs"] end subgraph IAM["Identity"] KC["Keycloak realm"] end GIT --> BUILD --> POD User["Reader"] --> POD POD --> KC
npm run build (Node ≥ 22.12); outputs dist/server/ + client assets.Dockerfile multi-stage; CMD runs node ./dist/server/entry.mjs.GET /health returns OK (excluded from auth middleware).KEYCLOAK_*, APP_BASE_URL, allowed redirect/logout URIs on the Keycloak client.npm installcp .env.example .env # fill Keycloak secretsnpm run dev # http://localhost:4321 → /en/Platform fit
Starlight as the docs shell with SSR, bilingual URLs, and Telekom chrome — ready for architecture reviews and workshops.
Auth model
End-to-end OIDC, JWT session, three-layer gating mapped to WIKIJS_* IAM groups.
Authoring depth
Representative SaaS doc patterns: structure, components, code, diagrams, API embed, static downloads.
Deploy path
Node adapter + Docker as a starting point for Kubernetes — not a full platform ops kit.
| Topic | Status |
|---|---|
| Full production content migration | Out of scope — demo copy only |
| Wiki.js / CMS authoring | Out of scope — git-based MDX only |
| Starlight Pagefind | Disabled — SSR + auth conflict |
| Private search metadata | PoC gap — see note above |
| ODS React runtime | Removed — CSS + custom components |
| Audit logging, rate limits, WAF | Not implemented |
| Automatic IAM / group provisioning | Manual Keycloak test users |
| SEO, analytics, cookie consent | Not configured |
| HA, CDN, preview environments | Single image / single region assumed |
| In-app content workflow | Git PR review only |
Production target: docs.dih.telekom.com remains the live knowledge base; this repo answers can we rebuild the experience on Starlight with the same auth and URL model?