From 13a8b234aa3e595791698bc9665f8c15d2b012a8 Mon Sep 17 00:00:00 2001 From: Daniel Vallance Date: Mon, 22 Jun 2026 15:48:22 +0100 Subject: [PATCH 1/2] WIP: Smart docs --- src/UnikraftCodeTabs.tsx | 83 +++++++++++++++++++++++++++++ src/personalization/controlplane.ts | 72 +++++++++++++++++++++++++ zudoku.config.tsx | 45 ++++++++-------- 3 files changed, 179 insertions(+), 21 deletions(-) create mode 100644 src/UnikraftCodeTabs.tsx create mode 100644 src/personalization/controlplane.ts diff --git a/src/UnikraftCodeTabs.tsx b/src/UnikraftCodeTabs.tsx new file mode 100644 index 0000000..57eeec2 --- /dev/null +++ b/src/UnikraftCodeTabs.tsx @@ -0,0 +1,83 @@ +import { + Children, + cloneElement, + isValidElement, + lazy, + Suspense, + type ReactElement, + type ReactNode, +} from "react"; +import { CodeTabPanel, type CodeTabPanelProps } from "zudoku/ui/CodeTabs"; +import { + ORG_PLACEHOLDER, + UNIKRAFT_TITLE, + useOrganizationName, +} from "./personalization/controlplane"; + +// Lazy-loaded to keep Shiki out of the initial bundle, mirroring Zudoku's own +// default CodeTabs registration. +const CodeTabs = lazy(() => + import("zudoku/ui/CodeTabs").then((m) => ({ default: m.CodeTabs })), +); + +interface UnikraftCodeTabsProps { + children?: ReactNode; + syncKey?: string; + hideIcon?: boolean; +} + +const isCodeTabPanel = ( + child: ReactNode, +): child is ReactElement => + isValidElement(child) && + (child.type as typeof CodeTabPanel).displayName === CodeTabPanel.displayName; + +/** Extracts the `title="..."` value from a fenced block's meta string. */ +const titleFromMeta = (meta?: string): string | undefined => + meta?.match(/title="([^"]*)"/)?.[1]; + +/** + * Drop-in replacement for Zudoku's `` that personalizes Unikraft CLI + * snippets for signed-in users. + * + * Every `title="unikraft"` panel containing the `` placeholder is + * rewritten with the user's actual organization slug. Legacy `kraft` panels, + * any other code, and the signed-out experience are left untouched, so the + * feature degrades gracefully when the user is not signed in or the + * organization cannot be resolved. + */ +export const UnikraftCodeTabs = ({ + children, + ...props +}: UnikraftCodeTabsProps) => { + const organizationName = useOrganizationName(); + + const personalizedChildren = organizationName + ? Children.map(children, (child) => { + if (!isCodeTabPanel(child)) return child; + + const { code, meta, title } = child.props; + const panelTitle = title ?? titleFromMeta(meta); + + if ( + panelTitle !== UNIKRAFT_TITLE || + typeof code !== "string" || + !code.includes(ORG_PLACEHOLDER) + ) { + return child; + } + + return cloneElement(child, { + code: code.replaceAll(ORG_PLACEHOLDER, organizationName), + }); + }) + : children; + + return ( + + {personalizedChildren} + + ); +}; + +export default UnikraftCodeTabs; diff --git a/src/personalization/controlplane.ts b/src/personalization/controlplane.ts new file mode 100644 index 0000000..e6d45ce --- /dev/null +++ b/src/personalization/controlplane.ts @@ -0,0 +1,72 @@ +import { useAuth, useZudoku } from "zudoku/hooks"; +import { useQuery } from "zudoku/react-query"; + +/** + * Base URL of the Unikraft Cloud control plane API. + * + * The personalization feature reads the signed-in user's organization from + * `GET {CONTROLPLANE_URL}/v1/auth`, authenticated with the user's OIDC access + * token. The token must carry the `org:metadata` scope, so the OIDC login + * configured in `zudoku.config.tsx` must request `scope: "openid org:metadata"`. + */ +export const CONTROLPLANE_URL = "https://cloud-console-pr-968.ukp-stable.apw.unikraft.internal"; + +/** Placeholder used throughout the docs to stand in for the user's org slug. */ +export const ORG_PLACEHOLDER = ""; + +/** Only code panels carrying this title are personalized. */ +export const UNIKRAFT_TITLE = "unikraft"; + +/** + * Relevant part of the `GET /v1/auth` response envelope. Every response carries + * a `status`; on failure `data` is absent and `status` is `"error"`. + */ +interface GetAuthorizationResponse { + status?: "success" | "error"; + message?: string; + data?: { + organization_name?: string; + organization_display_name?: string; + registry?: string; + }; +} + +/** + * Returns the signed-in user's organization slug, or `undefined` when the user + * is signed out, authentication is unavailable, or the request fails. + * + * The query is keyed by the user's subject so switching accounts refetches, and + * the shared key deduplicates the many code blocks rendered on a single page + * into a single request. + */ +export const useOrganizationName = (): string | undefined => { + const { isAuthEnabled, isAuthenticated, profile } = useAuth(); + const { authentication } = useZudoku(); + + const { data } = useQuery({ + queryKey: ["unikraft-organization", profile?.sub], + enabled: isAuthEnabled && isAuthenticated && Boolean(authentication), + staleTime: 5 * 60 * 1000, + retry: false, + queryFn: async (): Promise => { + if (!authentication) return undefined; + + const request = new Request(`${CONTROLPLANE_URL}/v1/auth`, { + headers: { Accept: "application/json" }, + }); + const response = await fetch(await authentication.signRequest(request)); + + const body = (await response + .json() + .catch(() => undefined)) as GetAuthorizationResponse | undefined; + + // The API wraps results in a status envelope; treat any non-success + // response as failure regardless of the HTTP status code. + if (body?.status !== "success") return undefined; + + return body.data?.organization_name || undefined; + }, + }); + + return data ?? undefined; +}; diff --git a/zudoku.config.tsx b/zudoku.config.tsx index 98ba33b..f38ec01 100644 --- a/zudoku.config.tsx +++ b/zudoku.config.tsx @@ -1,19 +1,12 @@ import type { ZudokuConfig } from "zudoku"; +import { UnikraftCodeTabs } from "./src/UnikraftCodeTabs"; const config: ZudokuConfig = { metadata: { title: "%s | Unikraft Cloud Docs", - description: - "Unikraft Cloud documentation: guides, platform reference, CLI, and API docs for the millisecond, Linux-based microVM cloud platform.", favicon: "/docs/favicon.ico", }, basePath: "/docs", - // Emit on every docs page (origin + basePath + route), - // and generate a sitemap so docs pages are discoverable/indexable by agents. - canonicalUrlOrigin: "https://unikraft.com", - sitemap: { - siteUrl: "https://unikraft.com", - }, site: { logo: { src: { light: "/logo-light.svg", dark: "/logo-dark.svg" }, @@ -56,6 +49,12 @@ const config: ZudokuConfig = { dark: "github-dark-high-contrast", }, }, + mdx: { + components: { + // Personalizes `` in Unikraft CLI code tabs for signed-in users. + CodeTabs: UnikraftCodeTabs, + }, + }, navigation: [ { type: "category", @@ -88,11 +87,6 @@ const config: ZudokuConfig = { "/features/cron-jobs", "/features/forking", "/features/branching", - "/features/managed-volumes", - "/features/checkpoints", - "/features/plugins", - "/features/custom-network-configuration", - "/features/annotations", ], }, { @@ -187,8 +181,6 @@ const config: ZudokuConfig = { "/guides/memcached1.6", // Memcached "/guides/minio", // Minio "/guides/mongodb", // MongoDB - "/guides/mysql", // MySQL - "/guides/neo4j", // Neo4j "/guides/httpserver-node21-nextjs", // Next.js HTTP Server "/guides/nginx", // Nginx "/guides/node24-karaoke", // Node AllKaraoke @@ -315,6 +307,7 @@ const config: ZudokuConfig = { collapsed: false, items: [ "/cli/unikraft/images", + "/cli/unikraft/images/build", "/cli/unikraft/images/copy", "/cli/unikraft/images/delete", "/cli/unikraft/images/get", @@ -400,9 +393,11 @@ const config: ZudokuConfig = { collapsed: false, items: [ "/cli/unikraft/volumes", + "/cli/unikraft/volumes/attach", "/cli/unikraft/volumes/clone", "/cli/unikraft/volumes/create", "/cli/unikraft/volumes/delete", + "/cli/unikraft/volumes/detach", "/cli/unikraft/volumes/edit", "/cli/unikraft/volumes/get", "/cli/unikraft/volumes/import", @@ -506,6 +501,20 @@ const config: ZudokuConfig = { "/cli/kraft/metro/list", ], }, + { + type: "category", + label: "kraft cloud scale", + icon: "arrow-up-1-0", + collapsed: false, + items: [ + "/cli/kraft/scale", + "/cli/kraft/scale/add", + "/cli/kraft/scale/get", + "/cli/kraft/scale/init", + "/cli/kraft/scale/remove", + "/cli/kraft/scale/reset", + ], + }, { type: "category", label: "kraft cloud service", @@ -563,12 +572,6 @@ const config: ZudokuConfig = { icon: "unplug", to: "/api/platform/v1", }, - { - type: "link", - label: "Glossary", - icon: "book-a", - to: "https://unikraft.com/glossary", - }, ], search: { type: "pagefind", From dd1d8bf755d0223ed717dff5c6d8df3a7ef63fc8 Mon Sep 17 00:00:00 2001 From: Daniel Vallance Date: Fri, 3 Jul 2026 17:15:11 +0100 Subject: [PATCH 2/2] WIP: attempt to fix env --- zudoku.config.tsx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/zudoku.config.tsx b/zudoku.config.tsx index f38ec01..78dec06 100644 --- a/zudoku.config.tsx +++ b/zudoku.config.tsx @@ -1,6 +1,9 @@ import type { ZudokuConfig } from "zudoku"; import { UnikraftCodeTabs } from "./src/UnikraftCodeTabs"; +const OIDC_CLIENT_ID = process.env.ZUDOKU_OIDC_CLIENT_ID; +const OIDC_ISSUER = process.env.ZUDOKU_OIDC_ISSUER; + const config: ZudokuConfig = { metadata: { title: "%s | Unikraft Cloud Docs", @@ -15,6 +18,22 @@ const config: ZudokuConfig = { }, showPoweredBy: false, }, + ...(OIDC_CLIENT_ID && OIDC_ISSUER + ? { + authentication: { + type: "openid" as const, + clientId: OIDC_CLIENT_ID, + issuer: OIDC_ISSUER, + scopes: [ + "openid", + "profile", + "email", + "docs:enterprise", + "org:metadata", + ], + }, + } + : {}), docs: { files: "/pages/**/*.{md,mdx}", defaultOptions: {