diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c3915e55d..babf8a3bc 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -33,7 +33,7 @@ }, "features": { "ghcr.io/devcontainers/features/go:1": { - "version": "1.25" + "version": "1.25.3" }, "ghcr.io/devcontainers/features/docker-outside-of-docker": {}, "ghcr.io/schlich/devcontainer-features/cypress:1": { diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8fa71ba65..6696343e1 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -86,3 +86,21 @@ updates: commit-message: prefix: chore include: scope +- package-ecosystem: npm + directory: '/apps/docs' + open-pull-requests-limit: 10 + schedule: + interval: weekly + time: "02:00" + groups: + prod: + dependency-type: production + dev: + dependency-type: development + commit-message: + prefix: chore + include: scope + labels: + - "npm" + - "dependencies" + - "documentation" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 3be436bcc..2aad51c43 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -5,13 +5,13 @@ on: branches: - 'main' paths-ignore: - - 'docs/**' + - 'apps/docs/content/**' pull_request: # The branches below must be a subset of the branches above branches: - 'main' paths-ignore: - - 'docs/**' + - 'apps/docs/content/**' concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} diff --git a/.gitignore b/.gitignore index b464368fb..94b8d77b5 100644 --- a/.gitignore +++ b/.gitignore @@ -57,19 +57,18 @@ console/.angular pkg/**/**.connect.go **.gen.go openapi/**/*.json +# multi-version proto artifacts +/proto/latest/ +/proto/v*/ /internal/api/assets/authz.go /internal/api/assets/router.go internal/api/ui/console/static !internal/api/ui/console/static/gitkeep -docs/docs/apis/auth -docs/docs/apis/admin -docs/docs/apis/mgmt -docs/docs/apis/system -docs/docs/apis/proto -**/.sass-cache +apps/docs/content/apis/assets/assets.mdx +apps/docs/content/reference/api/ /internal/api/ui/login/static/resources/themes/zitadel/css/zitadel.css /internal/api/ui/login/static/resources/themes/zitadel/css/zitadel.css.map -zitadel-*-* +/zitadel-*-* !apps/**/zitadel-*-* # local @@ -106,3 +105,5 @@ CLAUDE.md AGENTS.md .mcp.json .gemini/* + +docs_old \ No newline at end of file diff --git a/.golangci.yaml b/.golangci.yaml index 59cf2d392..158e45e0a 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -2,6 +2,8 @@ version: "2" run: concurrency: 4 go: "1.24" + skip-dirs: + - apps/docs linters: enable: - asciicheck @@ -121,6 +123,10 @@ issues: max-issues-per-linter: 0 max-same-issues: 0 new-from-rev: origin/main + exclude-rules: + - path: apps/docs/.* + linters: + - typecheck formatters: enable: - gci diff --git a/.nxignore b/.nxignore index 5d94a71cb..fd3a2de74 100644 --- a/.nxignore +++ b/.nxignore @@ -2,3 +2,5 @@ !console/src/app/proto/generated !internal/api/ui/console/static !.artifacts/bin/*/*/zitadel.local +!apps/docs/content/apis/assets/assets.mdx +!apps/docs/content/reference/api/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8e4054777..fe79cd639 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -129,7 +129,7 @@ The code consists of the following parts: | API definitions | Specifications of the API | [Protobuf](https://developers.google.com/protocol-buffers) | [./proto/zitadel](./proto/zitadel) | [Contribute to API](#contribute-to-api) | | Management Console | Frontend the user interacts with after log in | [Angular](https://angular.io), [Typescript](https://www.typescriptlang.org) | [./console](./console) | [Contribute to Frontend](#contribute-to-frontend) | | Login | Modern authentication UI built with Next.js | [Next.js](https://nextjs.org), [React](https://reactjs.org), [TypeScript](https://www.typescriptlang.org) | [./apps/login](./apps/login) | [Contribute to Frontend](#contribute-to-frontend) | -| Docs | Project documentation made with docusaurus | [Docusaurus](https://docusaurus.io/) | [./docs](./docs) | [Contribute to Frontend](#contribute-to-frontend) | +| Docs | Project documentation made with docusaurus | [Docusaurus](https://docusaurus.io/) | [./apps/docs](./apps/docs) | [Contribute to Frontend](#contribute-to-frontend) | | translations | Internationalization files for default languages | YAML | [./console](./console) and [./internal](./internal) | [Contribute Translations](#contribute-translations) | Please follow the guides to validate and test the code before you contribute. @@ -582,62 +582,9 @@ pnpm nx run @zitadel/client:build # Build after changes ### Contribute to Docs -Project documentation is made with Docusaurus and is located under [./docs](./docs). The documentation uses **pnpm** and **Nx** for development and build processes. +Project documentation is located under [./apps/docs](./apps/docs). +Please refer to the [Docs README](./apps/docs/README.md) for detailed instructions on how to contribute to the documentation. -To start developing, make sure you followed the [quick start](#quick-start) steps. - -#### Local Development - -```bash -# Start development server (recommended) -pnpm nx run @zitadel/docs:dev - -# Or start production server -pnpm nx run @zitadel/docs:prod -``` - -The Docs build process automatically: - -1. Downloads required protoc plugins -2. Generates gRPC documentation from proto files -3. Generates API documentation from OpenAPI specs -4. Copies configuration files -5. Builds the Docusaurus site - -#### Local testing - -The documentation server will be available at http://localhost:3000 with live reload for fast development feedback. - -#### Style guide - -- **Code with variables**: Make sure that code snippets can be used by setting environment variables, instead of manually replacing a placeholder. -- **Embedded files**: When embedding mdx files, make sure the template ist prefixed by "\_" (lowdash). The content will be rendered inside the parent page, but is not accessible individually (eg, by search). -- **Don't repeat yourself**: When using the same content in multiple places, save and manage the content as separate file and make use of embedded files to import it into other docs pages. -- **Embedded code**: You can embed code snippets from a repository. See the [plugin](https://github.com/saucelabs/docusaurus-theme-github-codeblock#usage) for usage. - -Following the [Google style guide](https://developers.google.com/style) is highly recommended. Its clear and concise guidelines ensure consistency and effective communication within the wider developer community. - -The style guide covers a lot of material, so their [highlights](https://developers.google.com/style/highlights) page provides an overview of its most important points. Some of the points stated in the highlights that we care about most are given below: - -- Be conversational and friendly without being frivolous. -- Use sentence case for document titles and section headings. -- Use active voice: make clear who's performing the action. -- Use descriptive link text. - -#### Docs pull request - -When making a pull request use `docs(): ` as title for the semantic release. -Scope can be left empty (omit the brackets) or refer to the top navigation sections. - -#### Pass Quality Checks - -Verify the Docs build correctly. - -```bash -pnpm nx run @zitadel/docs:build -``` - -Fix the quality checks, add new checks that cover your changes and mark your pull request as ready for review when the pipeline checks pass. ## Contribute Translations @@ -657,7 +604,7 @@ You also have to add some changes to the following files: - [Register Local File](./console/src/app/app.module.ts) - Import and register the Angular locale, register `i18n-iso-countries` locale - [Exclude from Angular prebundle](./console/angular.json) - Add `i18n-iso-countries/langs/.json` to `prebundle.exclude` - [Add Supported Language](./console/src/app/utils/language.ts) -- [Customized Text Docs](./docs/docs/guides/manage/customize/texts.md) +- [Customized Text Docs](./apps/docs/docs/guides/manage/customize/texts.md) - [Add language option](./internal/api/ui/login/static/templates/external_not_found_option.html) ### Login v2 (Next.js) @@ -737,7 +684,7 @@ The category shows which part of Zitadel is affected. - **category: backend**: The backend includes the APIs, event store, command and query side. This is developed in golang. - **category: ci**: ci is all about continuous integration and pipelines. - **category: design**: All about the ux/ui of Zitadel -- **category: docs**: Adjustments or new documentations, this can be found in the docs folder. +- **category: docs**: Adjustments or new documentations, this can be found in the apps/docs folder. - **category: frontend**: The frontend concerns on the one hand the Zitadel Management Console (Angular) and on the other hand the Login (gohtml) - **category: infra**: Infrastructure does include many different parts. E.g Terraform-provider, docker, metrics, etc. - **category: translation**: Everything concerning translations or new languages diff --git a/LICENSING.md b/LICENSING.md index 875b6cbea..67aff71b7 100644 --- a/LICENSING.md +++ b/LICENSING.md @@ -16,6 +16,7 @@ The following files and directories, including their subdirectories, are license ``` proto/ +apps/docs/ ``` diff --git a/README.md b/README.md index 4c995cbd2..91b88bf3b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

- Zitadel Logo - Zitadel Logo + Zitadel Logo + Zitadel Logo

@@ -27,7 +27,7 @@

- +

Are you searching for a user management tool that is quickly set up like Auth0 and open source like Keycloak? diff --git a/apps/api/project.json b/apps/api/project.json index e56a32dbc..f1208e8bb 100644 --- a/apps/api/project.json +++ b/apps/api/project.json @@ -20,7 +20,9 @@ "!{workspaceRoot}/**/integration_test/**/*" ], "pack": [ - { "env": "VERSION" }, + { + "env": "VERSION" + }, "runtime" ] }, @@ -65,10 +67,16 @@ ], "command": "bash -c 'CGO_ENABLED=0 go build -o .artifacts/bin/$(go env GOOS)/$(go env GOARCH)/zitadel.local -ldflags=\"-s -w\"'", "inputs": [ - { "runtime": "go env GOOS" }, - { "runtime": "go env GOARCH" }, + { + "runtime": "go env GOOS" + }, + { + "runtime": "go env GOARCH" + }, "runtime", - {"dependentTasksOutputFiles": "internal/api/ui/console/static/**/*"} + { + "dependentTasksOutputFiles": "internal/api/ui/console/static/**/*" + } ], "outputs": [ "{workspaceRoot}/.artifacts/bin/*/*/zitadel.local" @@ -82,13 +90,17 @@ "generate-statik" ] }, - "lint-install":{ + "lint-install": { "description": "Installs golangci-lint binary for linting. Using go install is not recommended in the official docs, because this can produce non-deterministic results.", "cache": true, "command": "curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b .artifacts/bin/$(go env GOOS)/$(go env GOARCH) v2.5.0", "inputs": [ - { "runtime": "go env GOOS" }, - { "runtime": "go env GOARCH" } + { + "runtime": "go env GOOS" + }, + { + "runtime": "go env GOARCH" + } ], "outputs": [ "{workspaceRoot}/.artifacts/bin/*/*/golangci-lint" @@ -137,8 +149,12 @@ "command": "go build -cover -race -tags integration -o .artifacts/bin/$(go env GOOS)/$(go env GOARCH)/zitadel.test main.go", "cache": true, "inputs": [ - { "runtime": "go env GOOS" }, - { "runtime": "go env GOARCH" }, + { + "runtime": "go env GOOS" + }, + { + "runtime": "go env GOARCH" + }, "sources", "{workspaceRoot}/internal/integration/**", "{workspaceRoot}/go*" @@ -193,8 +209,12 @@ ] }, "inputs": [ - { "runtime": "go env GOOS" }, - { "runtime": "go env GOARCH" }, + { + "runtime": "go env GOOS" + }, + { + "runtime": "go env GOARCH" + }, "sources", "{workspaceRoot}/internal/integration/**", "{workspaceRoot}/go*" @@ -215,7 +235,9 @@ "command": "find internal/api/ui/console/static -mindepth 1 ! -name 'gitkeep' -delete -print && cp -r -v console/dist/console/* internal/api/ui/console/static", "cache": true, "inputs": [ - {"dependentTasksOutputFiles": "console/dist/console/**/*"} + { + "dependentTasksOutputFiles": "console/dist/console/**/*" + } ], "outputs": [ "{workspaceRoot}/internal/api/ui/console/static" @@ -231,7 +253,6 @@ "GOBIN=${PWD}/.artifacts/bin/$(go env GOOS)/$(go env GOARCH) go install go.uber.org/mock/mockgen@v0.4.0", "GOBIN=${PWD}/.artifacts/bin/$(go env GOOS)/$(go env GOARCH) go install golang.org/x/tools/cmd/stringer@v0.36.0", "GOBIN=${PWD}/.artifacts/bin/$(go env GOOS)/$(go env GOARCH) go install github.com/rakyll/statik@v0.1.7", - "GOBIN=${PWD}/.artifacts/bin/$(go env GOOS)/$(go env GOARCH) go install github.com/bufbuild/buf/cmd/buf@v1.45.0", "GOBIN=${PWD}/.artifacts/bin/$(go env GOOS)/$(go env GOARCH) go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.35.1", "GOBIN=${PWD}/.artifacts/bin/$(go env GOOS)/$(go env GOARCH) go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.5.1", @@ -240,15 +261,18 @@ "GOBIN=${PWD}/.artifacts/bin/$(go env GOOS)/$(go env GOARCH) go install github.com/envoyproxy/protoc-gen-validate@v1.1.0", "GOBIN=${PWD}/.artifacts/bin/$(go env GOOS)/$(go env GOARCH) go install connectrpc.com/connect/cmd/protoc-gen-connect-go@v1.18.1", "GOBIN=${PWD}/.artifacts/bin/$(go env GOOS)/$(go env GOARCH) go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.35.1", - "GOBIN=${PWD}/.artifacts/bin/$(go env GOOS)/$(go env GOARCH) go install ./internal/protoc/protoc-gen-authoption", "GOBIN=${PWD}/.artifacts/bin/$(go env GOOS)/$(go env GOARCH) go install ./internal/protoc/protoc-gen-zitadel" ] }, "cache": true, "inputs": [ - { "runtime": "go env GOOS" }, - { "runtime": "go env GOARCH" }, + { + "runtime": "go env GOOS" + }, + { + "runtime": "go env GOARCH" + }, "{workspaceRoot}/internal/protoc/protoc-gen-authoption/**/*", "{workspaceRoot}/internal/protoc/protoc-gen-zitadel/**/*" ], @@ -257,7 +281,6 @@ "{workspaceRoot}/.artifacts/bin/*/*/mockgen", "{workspaceRoot}/.artifacts/bin/*/*/stringer", "{workspaceRoot}/.artifacts/bin/*/*/statik", - "{workspaceRoot}/.artifacts/bin/*/*/buf", "{workspaceRoot}/.artifacts/bin/*/*/protoc-gen-go", "{workspaceRoot}/.artifacts/bin/*/*/protoc-gen-go-grpc", @@ -265,7 +288,6 @@ "{workspaceRoot}/.artifacts/bin/*/*/protoc-gen-openapiv2", "{workspaceRoot}/.artifacts/bin/*/*/protoc-gen-validate", "{workspaceRoot}/.artifacts/bin/*/*/protoc-gen-connect-go", - "{workspaceRoot}/.artifacts/bin/*/*/protoc-gen-authoption", "{workspaceRoot}/.artifacts/bin/*/*/protoc-gen-zitadel" ] @@ -313,8 +335,12 @@ }, "cache": true, "inputs": [ - { "runtime": "go env GOOS" }, - { "runtime": "go env GOARCH" }, + { + "runtime": "go env GOOS" + }, + { + "runtime": "go env GOARCH" + }, "{workspaceRoot}/internal/statik/generate.go", "{workspaceRoot}/internal/notification/statik/generate.go", "{workspaceRoot}/internal/api/ui/login/static/resources/generate.go", @@ -332,7 +358,7 @@ "dependsOn": [ "generate-install" ], - "command": "mkdir -p docs/apis/assets && go run internal/api/assets/generator/asset_generator.go -directory=internal/api/assets/generator/ -assets=docs/apis/assets/assets.md", + "command": "mkdir -p apps/docs/content/apis/assets && go run internal/api/assets/generator/asset_generator.go -directory=internal/api/assets/generator/ -assets=apps/docs/content/apis/assets/assets.mdx", "cache": true, "inputs": [ "{workspaceRoot}/internal/api/assets/generator/asset_generator.go" @@ -340,7 +366,7 @@ "outputs": [ "{workspaceRoot}/internal/api/assets/authz.go", "{workspaceRoot}/internal/api/assets/router.go", - "{workspaceRoot}/docs/apis/assets/assets.md" + "{workspaceRoot}/apps/docs/content/apis/assets/assets.mdx" ] }, "generate-go": { @@ -372,7 +398,9 @@ "cache": true, "inputs": [ "pack", - { "env": "ZITADEL_VERSION" } + { + "env": "ZITADEL_VERSION" + } ], "outputs": [ "{workspaceRoot}/.artifacts/pack/zitadel-darwin-amd64.tar.gz", @@ -389,7 +417,9 @@ "cache": true, "inputs": [ "pack", - { "env": "ZITADEL_VERSION" } + { + "env": "ZITADEL_VERSION" + } ], "outputs": [ "{workspaceRoot}/.artifacts/pack/zitadel-darwin-arm64.tar.gz", @@ -406,7 +436,9 @@ "cache": true, "inputs": [ "pack", - { "env": "ZITADEL_VERSION" } + { + "env": "ZITADEL_VERSION" + } ], "outputs": [ "{workspaceRoot}/.artifacts/pack/zitadel-linux-amd64.tar.gz", @@ -423,7 +455,9 @@ "cache": true, "inputs": [ "pack", - { "env": "ZITADEL_VERSION" } + { + "env": "ZITADEL_VERSION" + } ], "outputs": [ "{workspaceRoot}/.artifacts/pack/zitadel-linux-arm64.tar.gz", @@ -440,7 +474,9 @@ "cache": true, "inputs": [ "pack", - { "env": "ZITADEL_VERSION" } + { + "env": "ZITADEL_VERSION" + } ], "outputs": [ "{workspaceRoot}/.artifacts/pack/zitadel-windows-amd64.tar.gz", @@ -457,7 +493,9 @@ "cache": true, "inputs": [ "pack", - { "env": "ZITADEL_VERSION" } + { + "env": "ZITADEL_VERSION" + } ], "outputs": [ "{workspaceRoot}/.artifacts/pack/zitadel-windows-arm64.tar.gz", @@ -465,4 +503,4 @@ ] } } -} +} \ No newline at end of file diff --git a/apps/docs/.gitignore b/apps/docs/.gitignore new file mode 100644 index 000000000..01c23dc09 --- /dev/null +++ b/apps/docs/.gitignore @@ -0,0 +1,39 @@ +# deps +/node_modules + +# generated content +.source +scanned-urls.json + +# test & build +/coverage +/.next/ +/out/ +/build +*.tsbuildinfo + +# misc +.DS_Store +*.pem +/.pnp +.pnp.js +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# others +.env*.local +.vercel +next-env.d.ts + +# generated api docs +/content/reference/api +/content/reference/api-v1 +/openapi/ + +# multi-version artifacts +/content/versions.json +/content/v*/ +/public/v* +/.temp/ +/.source diff --git a/apps/docs/README.md b/apps/docs/README.md new file mode 100644 index 000000000..4b38b4dbf --- /dev/null +++ b/apps/docs/README.md @@ -0,0 +1,83 @@ +# @zitadel/docs + +Documentation for Zitadel, built with [Fumadocs](https://fumadocs.dev) and [Next.js](https://nextjs.org). + +## Getting Started + +Ensure you have followed the [root quick start](../../CONTRIBUTING.md#quick-start) to set up dependencies. + +### Local Development + +Start the development server: + +```bash +pnpm nx run @zitadel/docs:dev +``` + +The site will be available at http://localhost:3000. + +### Scripts + +Key scripts for documentation workflows: + +| Script | Description | +| :--- | :--- | +| `dev` | Starts the development server. | +| `build` | Builds the production application. | +| `fetch:remote-content` | Fetches remote tags and referenced content. | +| `generate` | Runs all generation steps (`fetch:remote-content`, `generate:proto-docs`, `generate:api-reference`, `generate:index-pages`). | +| `check:links` | Validates content integrity (broken links, missing frontmatter, schema errors). | +| `check-types` | Validates typescript types. | +| `test` | Runs all validation steps (`check-types`, `check:links`). | +| `lint` | checks for code style and syntax errors (ESLint). | +| `clean` | Cleans the build output and generated files. | + +### Validation + +* **Code Quality**: Run `pnpm lint` to check for syntax and style issues in JS/TS/MDX files. +* **Content Integrity**: Run `pnpm check:links` to validate content structure, including: + * Broken internal links + * Missing required frontmatter (e.g., `title`) + * Image references + +## Contributing + +### Build Process + +The docs build process automatically handling the following steps via `generate`: +1. Downloads required protoc plugins. +2. Generates gRPC documentation from proto files. +3. Generates API documentation from OpenAPI specs. +4. Generates index files for directory structures. + +### Style Guide + +- **Variables**: Use environment variables in code snippets where possible. +- **Embedded Content**: Use `_filename.mdx` for content embedded in other pages (not indexed individually). +- **Code Embedding**: Use the `file` property in code blocks to embed code from the repo. +- **Voice**: Use active voice and sentence case for titles. + +Refer to the [Google Developer Style Guide](https://developers.google.com/style) for general guidelines. + +### Adding Content + +All documentation content is located in the `content` directory. Note that the system strictly accepts **only `.mdx` files**. + +To add a new page: +1. Create a `.mdx` file in the appropriate subdirectory of [`content`](./content). +2. Register the new page in the sidebar configuration at [`lib/sidebar-data.ts`](./lib/sidebar-data.ts) to make it accessible in the navigation. + + +### Pull Requests + +Use `docs(): ` for PR titles. +Pass quality checks before submitting: + +```bash +pnpm nx run @zitadel/docs:build +pnpm nx run @zitadel/docs:check:links +``` + +You can also run specific steps individually: +* `pnpm fetch:remote-content` +* `pnpm generate:api-reference` diff --git a/apps/docs/app/(home)/layout.tsx b/apps/docs/app/(home)/layout.tsx new file mode 100644 index 000000000..8d1530ec2 --- /dev/null +++ b/apps/docs/app/(home)/layout.tsx @@ -0,0 +1,7 @@ +import { HomeLayout } from 'fumadocs-ui/layouts/home'; +import { baseOptions } from '@/lib/layout.shared'; +import { ReactNode } from 'react'; + +export default function Layout({ children }: { children: ReactNode }) { + return {children}; +} diff --git a/apps/docs/app/[[...slug]]/layout.tsx b/apps/docs/app/[[...slug]]/layout.tsx new file mode 100644 index 000000000..1703f5029 --- /dev/null +++ b/apps/docs/app/[[...slug]]/layout.tsx @@ -0,0 +1,81 @@ +import { source, versionSource } from '@/lib/source'; +import { ReactNode } from 'react'; +import { DocsLayout } from 'fumadocs-ui/layouts/docs'; +import { baseOptions } from '@/lib/layout.shared'; +import { buildCustomTree } from '@/lib/custom-tree'; +import rawVersions from '@/content/versions.json'; + +const versions = rawVersions as any[]; +import { VersionSelector } from '@/components/version-selector'; + +export default async function Layout(props: { children: ReactNode; params: Promise<{ slug?: string[] }> }) { + const params = await props.params; + const slug = params.slug || []; + + // Determine current version and tail path + // If first slug segment matches a version param (e.g. v4.10), that's the version. + // Otherwise it's latest. + const versionParam = slug[0] && versions.find(v => v.param === slug[0]) ? slug[0] : undefined; + const currentVersion = versionParam || 'latest'; + + const allPages = [...source.getPages(), ...versionSource.getPages()]; + const labelsMap = new Map(); + allPages.forEach(p => { + if (p.data.sidebar_label) { + labelsMap.set(p.url, p.data.sidebar_label); + } + }); + + let tree = source.pageTree; + if (currentVersion !== 'latest') { + // Hoist the version folder to root to flatten sidebar + // versionSource.pageTree -> [ v4.10 folder, v4.9 folder ... ] + // We want the children of the specific version folder. + + const children = (versionSource.pageTree as any).children || []; + + const versionFolder = children.find((node: any) => { + if (node.type !== 'folder') return false; + // Check if the folder's index page belongs to this version + if (node.index?.url && node.index.url.includes(`/${currentVersion}`)) return true; + // Fallback: check first child if index missing + if (node.children?.[0]?.url?.includes(`/${currentVersion}`)) return true; + return false; + }); + + if (versionFolder) { + // Hoist children + const hoistedTree = { + name: versionFolder.name, + children: versionFolder.children, + } as any; + + // Apply custom sidebar structure (from local sidebar-data.ts) + // We strip the prefix '/v4.10' so the lookup matches generic keys 'guides/...' + // The logs showed the URLs start with /[version] not /docs/[version] + const prefix = currentVersion === 'latest' ? '/docs' : `/${currentVersion}`; + tree = buildCustomTree(hoistedTree, { + stripPrefix: prefix, + labels: labelsMap + }); + } else { + tree = buildCustomTree(versionSource.pageTree, { labels: labelsMap }); + } + } else { + tree = buildCustomTree(source.pageTree, { labels: labelsMap }); + } + + return ( + + ), + }} + > + {props.children} + + ); +} diff --git a/apps/docs/app/[[...slug]]/page.tsx b/apps/docs/app/[[...slug]]/page.tsx new file mode 100644 index 000000000..bd00570d7 --- /dev/null +++ b/apps/docs/app/[[...slug]]/page.tsx @@ -0,0 +1,79 @@ +import { getPageImage, getPage, source } from '@/lib/source'; +import { + DocsBody, + DocsDescription, + DocsPage, + DocsTitle, +} from 'fumadocs-ui/layouts/docs/page'; +import { notFound } from 'next/navigation'; +import { getMDXComponents } from '@/mdx-components'; +import type { Metadata } from 'next'; +import { createRelativeLink } from 'fumadocs-ui/mdx'; +import { Callout } from 'fumadocs-ui/components/callout'; +import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; +import { Feedback } from '@/components/feedback'; + +export default async function Page(props: any) { + const params = await props.params; + const { page, source: pageSource } = getPage(params.slug); + if (!page) notFound(); + + const MDX = page.data.body; + + return ( + + {page.data.title} + {page.data.description} + + + + + + ); +} + +export const dynamicParams = true; +export const revalidate = 3600; + +export async function generateStaticParams() { + return source.generateParams(); +} + +export async function generateMetadata( + props: any, +): Promise { + const params = await props.params; + const { page } = getPage(params.slug); + if (!page) notFound(); + const baseUrl = 'https://zitadel.com/docs'; + const url = params.slug ? `${baseUrl}/${params.slug.join('/')}` : baseUrl; + + let canonicalUrl = url; + + if (params.slug?.[0]?.startsWith('v')) { + const unversionedSlug = params.slug.slice(1); + const unversionedPage = source.getPage(unversionedSlug); + if (unversionedPage) { + canonicalUrl = `${baseUrl}${unversionedPage.url === '/' ? '' : unversionedPage.url}`; + } + } + + return { + title: page.data.title, + description: page.data.description, + alternates: { + canonical: canonicalUrl, + }, + openGraph: { + images: getPageImage(page).url, + }, + }; +} diff --git a/apps/docs/app/api/search/route.ts b/apps/docs/app/api/search/route.ts new file mode 100644 index 000000000..7ba7e8231 --- /dev/null +++ b/apps/docs/app/api/search/route.ts @@ -0,0 +1,7 @@ +import { source } from '@/lib/source'; +import { createFromSource } from 'fumadocs-core/search/server'; + +export const { GET } = createFromSource(source, { + // https://docs.orama.com/docs/orama-js/supported-languages + language: 'english', +}); diff --git a/apps/docs/app/global.css b/apps/docs/app/global.css new file mode 100644 index 000000000..c194edee4 --- /dev/null +++ b/apps/docs/app/global.css @@ -0,0 +1,408 @@ +@import 'tailwindcss'; +@import 'fumadocs-ui/css/neutral.css'; +@import 'fumadocs-ui/css/preset.css'; +@import 'fumadocs-openapi/css/preset.css'; + +@theme { + /* 1. REGISTER STANDARD UTILITIES */ + --color-background: hsl(var(--background)); + --color-foreground: hsl(var(--foreground)); + --color-card: hsl(var(--card)); + --color-card-foreground: hsl(var(--card-foreground)); + --color-popover: hsl(var(--popover)); + --color-popover-foreground: hsl(var(--popover-foreground)); + --color-primary: hsl(var(--primary)); + --color-primary-foreground: hsl(var(--primary-foreground)); + --color-secondary: hsl(var(--secondary)); + --color-secondary-foreground: hsl(var(--secondary-foreground)); + --color-muted: hsl(var(--muted)); + --color-muted-foreground: hsl(var(--muted-foreground)); + --color-accent: hsl(var(--accent)); + --color-accent-foreground: hsl(var(--accent-foreground)); + --color-destructive: hsl(var(--destructive)); + --color-destructive-foreground: hsl(var(--destructive-foreground)); + --color-border: hsl(var(--border)); + --color-input: hsl(var(--input)); + --color-ring: hsl(var(--ring)); + + /* 2. MAP FUMADOCS VARIABLES */ + --color-fd-primary: var(--color-primary); + --color-fd-primary-foreground: var(--color-primary-foreground); + --color-fd-background: var(--color-background); + --color-fd-foreground: var(--color-foreground); + --color-fd-card: var(--color-card); + --color-fd-card-foreground: var(--color-card-foreground); + --color-fd-popover: var(--color-popover); + --color-fd-popover-foreground: var(--color-popover-foreground); + --color-fd-muted: var(--color-muted); + --color-fd-muted-foreground: var(--color-muted-foreground); + --color-fd-accent: var(--color-accent); + --color-fd-accent-foreground: var(--color-accent-foreground); + --color-fd-border: var(--color-border); + --color-fd-ring: var(--color-ring); + + --color-zitadelpink: #ff2069; + --font-sans: var(--font-lato), "Lato", "ui-sans-serif", "system-ui"; +} + +/* --- LIGHT MODE VARIABLES --- */ +:root { + --primary: 230 59% 58%; + --primary-foreground: 0 0% 100%; + + --background: 0 0% 98%; + --foreground: 0 0% 10%; + + --card: 0 0% 93%; + --card-foreground: 0 0% 10%; + + --muted: 0 0% 96%; + --muted-foreground: 0 0% 45%; + + --accent: 0 0% 92%; + --accent-foreground: 230 59% 58%; + + --secondary: 240 4.8% 95.9%; + --secondary-foreground: 240 5.9% 10%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + + --border: 0 0% 90%; + --input: 0 0% 90%; + --ring: 230 59% 58%; + + --radius: 0.5rem; + --overlaycolor: #6c90b420; +} + +/* --- DARK MODE VARIABLES --- */ +:root[class~="dark"], [data-theme="dark"] { + --primary: 340 100% 56%; + --primary-foreground: 0 0% 100%; + + --background: 235 45% 14%; + --foreground: 0 0% 98%; + + --card: 220 40% 25%; + --card-foreground: 0 0% 98%; + + --muted: 230 43% 15%; + --muted-foreground: 230 20% 70%; + + --accent: 230 43% 20%; + --accent-foreground: 340 100% 65%; + + --secondary: 240 3.7% 15.9%; + --secondary-foreground: 0 0% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + + --border: 230 43% 25%; + --input: 230 43% 25%; + --ring: 340 100% 56%; + + --overlaycolor: #ffffff15; + + /* FORCE OVERRIDE for Fumadocs Variables */ + --color-fd-background: hsl(var(--background)); + --color-fd-foreground: hsl(var(--foreground)); + --color-fd-primary: hsl(var(--primary)); + --color-fd-primary-foreground: hsl(var(--primary-foreground)); + --color-fd-card: hsl(var(--card)); + --color-fd-card-foreground: hsl(var(--card-foreground)); + --color-fd-popover: hsl(var(--card)); + --color-fd-popover-foreground: hsl(var(--foreground)); + --color-fd-muted: hsl(var(--muted)); + --color-fd-muted-foreground: hsl(var(--muted-foreground)); + --color-fd-accent: hsl(var(--accent)); + --color-fd-accent-foreground: hsl(var(--accent-foreground)); + --color-fd-border: hsl(var(--border)); + --color-fd-ring: hsl(var(--ring)); +} + +@layer components { + + /* 1. VISIBILITY HELPERS */ + .hideondark { display: block !important; } + .hideonlight { display: none !important; } + + :root[class~="dark"] .hideondark, [data-theme="dark"] .hideondark { + display: none !important; + } + :root[class~="dark"] .hideonlight, [data-theme="dark"] .hideonlight { + display: block !important; + } + + /* 2. MAIN CARD LAYOUT (Tile Wrapper) */ + .tile-wrapper { + @apply flex flex-row flex-wrap gap-6 my-6; + } + + .tile-wrapper > * { + @apply flex flex-col items-center text-center rounded-lg border relative; + + background-color: hsl(var(--card)) !important; + border-color: hsl(var(--border)); + + justify-content: flex-start; + min-width: 160px; + min-height: 180px; + padding: 2rem; + padding-bottom: 3rem; + } + + .tile-wrapper > * > :last-child { + margin-top: auto; + } + + .tile-wrapper > * > h3, + .tile-wrapper > * > h4, + .tile-wrapper > * > strong { + @apply mt-0; + } + + /* 3. ICON STYLING */ + .custom-rounded { + @apply flex items-center justify-center p-2 rounded-full w-14 h-14 shrink-0 m-0; + } + .custom-rounded svg { + @apply fill-white h-7 w-7; + } + .custom-rounded i { + @apply text-white; + } + + /* Gradients */ + .custom-rounded-start { background: linear-gradient(40deg, #059669 30%, #047857); } + .custom-rounded-password { background: linear-gradient(40deg, #f59e0b 30%, #b45309); } + .custom-rounded-login, .custom-rounded-register { background: linear-gradient(40deg, #059669 30%, #047857); } + .custom-rounded-privatelabel, .custom-rounded-phone, .custom-rounded-email, .custom-rounded-storage, .custom-rounded-service { background: linear-gradient(40deg, #ff2069 30%, #cc1a54); } + .custom-rounded-split { background: linear-gradient(40deg, #ff2069, #cc1a54); } + .custom-rounded-texts, .custom-rounded-help, .custom-rounded-architecture { background: linear-gradient(40deg, #dc2626 30%, #db2777); } + .custom-rounded-system, .custom-rounded-apis, .custom-rounded-policy, .custom-rounded-instance { background: linear-gradient(40deg, #1f2937, #111827); } + + /* 4. B2B / DIAGRAM LAYOUTS (Fixes the B2B Component) */ + .flexrowbetween { + @apply flex flex-row items-center justify-around gap-4 my-8 w-full; + } + + .arrowright { + @apply w-8 h-8 shrink-0 text-muted-foreground; + } + + .b2borg, .b2bproject, .b2bapp, .b2buser { + @apply rounded-xl p-4 border text-left; + background: var(--overlaycolor); + border-color: hsl(var(--border)); + } + + .b2buser { @apply mt-4; } + + .b2borg span, .b2bproject span, .b2bapp span, .b2buser span { + @apply ml-4 mb-2 block font-semibold; + } + + /* Specific styling for the role text */ + .b2bprojectrole { + @apply mt-2 ml-4 block text-sm opacity-80 font-normal; + } + + /* 5. FOOTER / PAGINATION CARDS */ + a.\@max-lg\:col-span-full { + @apply flex flex-col p-6 rounded-xl border transition-all no-underline; + background-color: hsl(var(--card)) !important; + border-color: hsl(var(--border)) !important; + } + + a.\@max-lg\:col-span-full:hover { + @apply shadow-sm; + border-color: hsl(var(--primary)) !important; + background-color: hsl(var(--card)) !important; + } + + a.\@max-lg\:col-span-full > div:first-child { + @apply flex items-center gap-3; + } + + a.\@max-lg\:col-span-full svg { + @apply w-5 h-5 m-0; + } + + a.\@max-lg\:col-span-full p { + margin-bottom: 0 !important; + margin-top: 0 !important; + } + + a.\@max-lg\:col-span-full p.text-fd-muted-foreground { + @apply mt-2 text-sm opacity-80; + } + + /* 6. VIDEO PLAYER WRAPPER */ + /* Fixes aspect ratio for ReactPlayer embeds */ + .player-wrapper { + @apply relative mb-8; + padding-top: 56.25%; /* 16:9 Aspect Ratio */ + } + .react-player { + @apply absolute top-0 left-0; + } + + /* 7. GET STARTED BUTTON */ + .get-started { + @apply inline-flex items-center justify-center px-6 py-2 rounded-full font-bold border-2 transition-all; + border-color: #ff2069; + background-color: #ff2069; + color: white; + } + .get-started:hover { + @apply opacity-90; + color: white !important; + } + + /* Target any div that uses the callout color variable */ + div[style*="--callout-color"] { + @apply rounded-lg shadow-sm mb-4; + + /* Force Docusaurus Layout: Left Border Only */ + border: 1px solid rgba(0,0,0,0.05) !important; + border-left-width: 4px !important; + + /* Hide the default Fumadocs vertical colored bar inside */ + & > div[role="none"] { + display: none !important; + } + } + + /* --- INFO (Blue) --- */ + /* We match the variable name in the style string */ + div[style*="--color-fd-info"] { + background-color: #eff6ff !important; + border-left-color: #3b82f6 !important; + color: #1e3a8a !important; + } + div[style*="--color-fd-info"] svg { + color: #3b82f6 !important; + fill: transparent !important; /* Remove fill to look cleaner */ + } + /* Dark Mode Info */ + :is(.dark div[style*="--color-fd-info"]) { + background-color: rgba(59, 130, 246, 0.1) !important; + color: #dbeafe !important; + border-top-color: rgba(255,255,255,0.05) !important; + border-right-color: rgba(255,255,255,0.05) !important; + border-bottom-color: rgba(255,255,255,0.05) !important; + } + + /* --- WARNING (Amber) --- */ + div[style*="--color-fd-warn"] { + background-color: #fffbeb !important; + border-left-color: #f59e0b !important; + color: #78350f !important; + } + div[style*="--color-fd-warn"] svg { + color: #f59e0b !important; + fill: transparent !important; + } + /* Dark Mode Warning */ + :is(.dark div[style*="--color-fd-warn"]) { + background-color: rgba(245, 158, 11, 0.1) !important; + color: #fef3c7 !important; + border-top-color: rgba(255,255,255,0.05) !important; + border-right-color: rgba(255,255,255,0.05) !important; + border-bottom-color: rgba(255,255,255,0.05) !important; + } + + /* --- ERROR (Red) --- */ + div[style*="--color-fd-error"] { + background-color: #fef2f2 !important; + border-left-color: #ef4444 !important; + color: #7f1d1d !important; + } + div[style*="--color-fd-error"] svg { + color: #ef4444 !important; + fill: transparent !important; + } + /* Dark Mode Error */ + :is(.dark div[style*="--color-fd-error"]) { + background-color: rgba(239, 68, 68, 0.12) !important; + color: #fee2e2 !important; + border-top-color: rgba(255,255,255,0.05) !important; + border-right-color: rgba(255,255,255,0.05) !important; + border-bottom-color: rgba(255,255,255,0.05) !important; + } + + /* --- TITLE BOLD --- */ + div[style*="--callout-color"] strong, + div[style*="--callout-color"] p:first-child strong { + font-weight: 700 !important; + } +} + +@layer base { + /* HEADINGS */ + h1, h2, h3, h4, h5, h6 { + font-family: var(--font-sans); + color: hsl(var(--foreground)) !important; + margin-bottom: 1rem; + letter-spacing: -0.025em; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + + h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { + color: inherit !important; + text-decoration: none !important; + } + + h1 { + font-size: 3.5rem !important; + font-weight: 700 !important; + line-height: 1.1 !important; + margin-bottom: 1.5rem !important; + } + + h2 { + font-size: 2.25rem !important; + font-weight: 600 !important; + margin-top: 3rem !important; + margin-bottom: 1rem !important; + padding-bottom: 0.5rem !important; + border-bottom: 1px solid hsl(var(--border)) !important; + } + + h3 { + font-size: 2rem !important; + font-weight: 600 !important; + margin-top: 2.5rem !important; + margin-bottom: 1rem !important; + } + + h4 { + font-size: 1.5rem !important; + font-weight: 600 !important; + margin-top: 2.5rem !important; + margin-bottom: 1rem !important; + } + + p { + font-size: 1rem; + line-height: 1.7; + margin-bottom: 1.5rem; + } + + html, body { + background-color: hsl(var(--background)); + color: hsl(var(--foreground)); + } + + a { + color: hsl(var(--primary)); + text-decoration: none; + transition: opacity 0.2s; + } + a:hover { + opacity: 0.8; + } +} \ No newline at end of file diff --git a/apps/docs/app/layout.config.tsx b/apps/docs/app/layout.config.tsx new file mode 100644 index 000000000..58cb9e3d1 --- /dev/null +++ b/apps/docs/app/layout.config.tsx @@ -0,0 +1,29 @@ +import { type BaseLayoutProps } from 'fumadocs-ui/layouts/shared'; +import { BookOpen, Code2, Server } from 'lucide-react'; + +export const baseOptions: BaseLayoutProps = { + nav: { + title: 'ZITADEL Docs', + }, + links: [ + { + text: 'Guides', + url: '/', + active: 'nested-url', + icon: , + }, + { + text: 'APIs', + url: '/apis/introduction', + active: 'nested-url', + icon: , + }, + { + text: 'Legal', + url: '/legal/terms-of-service', + active: 'nested-url', + icon: , // Using Server icon as placeholder or pick another one + }, + ], + githubUrl: 'https://github.com/zitadel/zitadel', +}; diff --git a/apps/docs/app/layout.tsx b/apps/docs/app/layout.tsx new file mode 100644 index 000000000..f894bd4cc --- /dev/null +++ b/apps/docs/app/layout.tsx @@ -0,0 +1,25 @@ +import './global.css'; +import type { Metadata } from 'next'; +import { Providers } from './providers'; + +export const metadata: Metadata = { + metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'), + icons: { + other: [ + { + rel: 'stylesheet', + url: '/docs/img/icons/line-awesome/css/line-awesome.min.css', + }, + ], + }, +}; + +export default function Layout({ children }: any) { + return ( + + + {children} + + + ); +} diff --git a/apps/docs/app/llms-full.txt/route.ts b/apps/docs/app/llms-full.txt/route.ts new file mode 100644 index 000000000..d494d2cbb --- /dev/null +++ b/apps/docs/app/llms-full.txt/route.ts @@ -0,0 +1,10 @@ +import { getLLMText, source } from '@/lib/source'; + +export const revalidate = false; + +export async function GET() { + const scan = source.getPages().map(getLLMText); + const scanned = await Promise.all(scan); + + return new Response(scanned.join('\n\n')); +} diff --git a/apps/docs/app/og/docs/[...slug]/route.tsx b/apps/docs/app/og/docs/[...slug]/route.tsx new file mode 100644 index 000000000..b88c88389 --- /dev/null +++ b/apps/docs/app/og/docs/[...slug]/route.tsx @@ -0,0 +1,28 @@ +import { ImageResponse } from 'next/og'; +import { notFound } from 'next/navigation'; +import { source } from '@/lib/source'; +import { generate as DefaultImage } from 'fumadocs-ui/og'; + +export const revalidate = false; + +export async function GET(request: Request, context: any) { + const { slug } = context.params; + const page = source.getPage(slug.slice(0, -1)); + if (!page) notFound(); + + return new ImageResponse( + , + { + width: 1200, + height: 630, + }, + ); +} + +export function generateStaticParams() { + return []; +} diff --git a/apps/docs/app/providers.tsx b/apps/docs/app/providers.tsx new file mode 100644 index 000000000..f143a4231 --- /dev/null +++ b/apps/docs/app/providers.tsx @@ -0,0 +1,26 @@ +'use client'; + +import { RootProvider } from 'fumadocs-ui/provider/next'; +import dynamic from "next/dynamic"; +import { ThemeProvider } from 'next-themes'; + +import MixpanelProvider from '@/components/mixpanel-provider'; +import PlausibleProvider from '@/components/plausible-provider'; + +const SearchDialog = dynamic(() => import("@/components/inkeep-search")); + +export function Providers({ children }: { children: React.ReactNode }) { + return ( + + + + {children} + + + ); +} diff --git a/apps/docs/app/sitemap.ts b/apps/docs/app/sitemap.ts new file mode 100644 index 000000000..07aa3912b --- /dev/null +++ b/apps/docs/app/sitemap.ts @@ -0,0 +1,27 @@ +import { MetadataRoute } from 'next'; +import { source } from '@/lib/source'; + +export default function sitemap(): MetadataRoute.Sitemap { + const baseUrl = 'https://zitadel.com/docs'; + + const docsPages = source.getPages().map((page) => ({ + url: `${baseUrl}${page.url === '/' ? '' : page.url}`, + lastModified: new Date(), + changeFrequency: 'weekly' as const, + priority: 0.8, + })); + + + + return [ + { + url: baseUrl, + lastModified: new Date(), + changeFrequency: 'daily' as const, + priority: 1, + }, + ...docsPages, + ].filter((item, index, self) => + index === self.findIndex((t) => t.url === item.url) + ); +} diff --git a/apps/docs/buf.gen.yaml b/apps/docs/buf.gen.yaml new file mode 100644 index 000000000..0b6ba85fe --- /dev/null +++ b/apps/docs/buf.gen.yaml @@ -0,0 +1,6 @@ +version: v2 +plugins: + - remote: buf.build/community/sudorandom-connect-openapi:v0.19.1 + out: . + opt: + - format=json diff --git a/apps/docs/components/PermissionTable/index.jsx b/apps/docs/components/PermissionTable/index.jsx new file mode 100644 index 000000000..ee82a33f5 --- /dev/null +++ b/apps/docs/components/PermissionTable/index.jsx @@ -0,0 +1,111 @@ +import React, { useMemo } from 'react'; +import yaml from 'js-yaml'; +import { Check, X } from 'lucide-react'; +import { cn } from '@/utils/cn'; // Assuming utils/cn exists or I'll use clsx/tailwind-merge directly if not sure + +// Fallback utility if generic cn doesn't exist, checking imports first is safer but I'll inline a simple merge if needed +// Checking package.json I saw clsx and tailwind-merge are dependencies. + +function classNames(...classes) { + return classes.filter(Boolean).join(' '); +} + +export default function PermissionTable({ data }) { + const { roles, permissions, matrix } = useMemo(() => { + if (!data) return { roles: [], permissions: [], matrix: {} }; + + let parsedData = data; + if (typeof data === 'string') { + try { + parsedData = yaml.load(data); + } catch (e) { + console.error('Failed to parse YAML data:', e); + return { roles: [], permissions: [], matrix: {} }; + } + } + + // 1. Identify all sources. + const sources = [ + parsedData?.InternalAuthZ?.RolePermissionMappings, + parsedData?.SystemAuthZ?.RolePermissionMappings, + ].filter(Boolean).flat(); + + // 2. Aggregate Data + const roleMap = new Map(); + const allPermissions = new Set(); + + sources.forEach((mapping) => { + const roleName = mapping.Role; + const rolePerms = mapping.Permissions || []; + + if (!roleMap.has(roleName)) { + roleMap.set(roleName, new Set()); + } + + rolePerms.forEach((perm) => { + roleMap.get(roleName).add(perm); + allPermissions.add(perm); + }); + }); + + // 3. Sort for display + const sortedRoles = Array.from(roleMap.keys()).sort(); + const sortedPermissions = Array.from(allPermissions).sort(); + + return { + roles: sortedRoles, + permissions: sortedPermissions, + matrix: roleMap, + }; + }, [data]); + + if (!roles.length) { + return
No permission data available.
; + } + + return ( +
+ + + + + {roles.map((role) => ( + + ))} + + + + {permissions.map((perm) => ( + + + {roles.map((role) => { + const hasPermission = matrix.get(role).has(perm); + return ( + + ); + })} + + ))} + +
+ Permission + + {role.replace(/_/g, ' ')} +
+ {perm} + + {hasPermission ? ( + + ) : ( + No + )} +
+
+ ); +} \ No newline at end of file diff --git a/apps/docs/components/api-page.client.tsx b/apps/docs/components/api-page.client.tsx new file mode 100644 index 000000000..0564d3da8 --- /dev/null +++ b/apps/docs/components/api-page.client.tsx @@ -0,0 +1,4 @@ +'use client'; +import { defineClientConfig } from 'fumadocs-openapi/ui/client'; + +export default defineClientConfig(); diff --git a/apps/docs/components/api-page.tsx b/apps/docs/components/api-page.tsx new file mode 100644 index 000000000..a16980b39 --- /dev/null +++ b/apps/docs/components/api-page.tsx @@ -0,0 +1,27 @@ +import { createOpenAPI } from 'fumadocs-openapi/server'; +import { createAPIPage } from 'fumadocs-openapi/ui'; +import path from 'path'; +import client from './api-page.client'; + +export function APIPage({ document, operations }: { document: string; operations: any[] }) { + + + if (!path.isAbsolute(document) && !document.startsWith('openapi/')) { + throw new Error(`APIPage document path MUST start with 'openapi/'. Received: ${document}`); + } + + const absoluteDocument = path.isAbsolute(document) + ? document + : path.join(process.cwd(), 'openapi', document.slice('openapi/'.length)); + + const openapi = createOpenAPI({ + input: [absoluteDocument], + }); + + const InnerAPIPage = createAPIPage(openapi, { + client, + }); + + return ; +} + diff --git a/docs/src/components/apicard.jsx b/apps/docs/components/apicard.jsx similarity index 93% rename from docs/src/components/apicard.jsx rename to apps/docs/components/apicard.jsx index c8025ae1d..8c6c0cae6 100644 --- a/docs/src/components/apicard.jsx +++ b/apps/docs/components/apicard.jsx @@ -1,6 +1,6 @@ import React from "react"; -import styles from "../css/apicard.module.css"; +import styles from "@/css/apicard.module.css"; export function ApiCard({ type, children }) { let classes = ""; diff --git a/docs/src/components/b2b.jsx b/apps/docs/components/b2b.jsx similarity index 100% rename from docs/src/components/b2b.jsx rename to apps/docs/components/b2b.jsx diff --git a/docs/src/components/benchmark_chart.jsx b/apps/docs/components/benchmark_chart.jsx similarity index 99% rename from docs/src/components/benchmark_chart.jsx rename to apps/docs/components/benchmark_chart.jsx index cf93a842e..3bb3a8224 100644 --- a/docs/src/components/benchmark_chart.jsx +++ b/apps/docs/components/benchmark_chart.jsx @@ -1,3 +1,4 @@ +"use client"; import React from "react"; import Chart from "react-google-charts"; diff --git a/docs/src/components/column.jsx b/apps/docs/components/column.jsx similarity index 100% rename from docs/src/components/column.jsx rename to apps/docs/components/column.jsx diff --git a/apps/docs/components/docusaurus/admonition.tsx b/apps/docs/components/docusaurus/admonition.tsx new file mode 100644 index 000000000..509d7de2d --- /dev/null +++ b/apps/docs/components/docusaurus/admonition.tsx @@ -0,0 +1,19 @@ +import { Callout } from 'fumadocs-ui/components/callout'; + +export default function Admonition({ type, title, children, icon }: any) { + let calloutType: 'info' | 'warn' | 'error' = 'info'; + switch (type) { + case 'note': calloutType = 'info'; break; + case 'tip': calloutType = 'info'; break; + case 'info': calloutType = 'info'; break; + case 'warning': calloutType = 'warn'; break; + case 'danger': calloutType = 'error'; break; + case 'caution': calloutType = 'warn'; break; + } + + return ( + + {children} + + ); +} diff --git a/apps/docs/components/docusaurus/browser-only.tsx b/apps/docs/components/docusaurus/browser-only.tsx new file mode 100644 index 000000000..ac851664f --- /dev/null +++ b/apps/docs/components/docusaurus/browser-only.tsx @@ -0,0 +1,12 @@ +"use client"; +import { useEffect, useState } from 'react'; + +export default function BrowserOnly({ children, fallback }: any) { + const [mounted, setMounted] = useState(false); + + useEffect(() => setMounted(true), []); + + if (!mounted) return fallback || null; + + return children(); +} diff --git a/apps/docs/components/docusaurus/code-block.tsx b/apps/docs/components/docusaurus/code-block.tsx new file mode 100644 index 000000000..eccba78d3 --- /dev/null +++ b/apps/docs/components/docusaurus/code-block.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +export default function CodeBlock({ children, language, title, className, showLineNumbers: _showLineNumbers }: any) { + return ( +
+ {title &&
{title}
} +
+        
+          {children}
+        
+      
+
+ ); +} diff --git a/apps/docs/components/docusaurus/doc-card-list.tsx b/apps/docs/components/docusaurus/doc-card-list.tsx new file mode 100644 index 000000000..6f009e398 --- /dev/null +++ b/apps/docs/components/docusaurus/doc-card-list.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Cards, Card } from 'fumadocs-ui/components/card'; + +export default function DocCardList({ items }: { items?: any[] }) { + // If items are provided (manual list), render them + if (items) { + return ( + + {items.map((item, index) => ( + + ))} + + ); + } + + // If no items, it's likely an auto-generated list for the current sidebar category. + // Fumadocs doesn't have a direct equivalent to "current sidebar category items" in a component easily accessible here without context. + // For now, we'll render a placeholder or nothing to avoid build errors. + // In a real migration, we'd use `useTreeContext` or similar if available, or rely on Fumadocs' index page generation. + return ( +
+
+
DocCardList Placeholder
+
+
+

Auto-generated card lists are not yet fully supported in the migration.

+
+
+ ); +} diff --git a/apps/docs/components/docusaurus/tabs.tsx b/apps/docs/components/docusaurus/tabs.tsx new file mode 100644 index 000000000..82d597037 --- /dev/null +++ b/apps/docs/components/docusaurus/tabs.tsx @@ -0,0 +1,58 @@ +import { Tabs as FumadocsTabs, Tab as FumadocsTab } from 'fumadocs-ui/components/tabs'; +import React, { isValidElement } from 'react'; + +export function TabItem({ value, label: _label, children, default: _isDefault, className, attributes }: any) { + return {children}; +} + +export default function Tabs({ children, defaultValue, values, groupId, className }: any) { + let items: string[] = []; + + const childrenArray = React.Children.toArray(children); + + if (values) { + items = values.map((v: any) => v.label); + } else { + childrenArray.forEach((child) => { + if (isValidElement(child)) { + const props = child.props as any; + const label = props.label || props.value; + items.push(label); + } + }); + } + + const newChildren = childrenArray.map((child, index) => { + if (isValidElement(child)) { + const props = child.props as any; + let label = props.label; + if (!label && values) { + const valObj = values.find((v: any) => v.value === props.value); + if (valObj) label = valObj.label; + } + if (!label) label = props.value; + + return {props.children}; + } + return child; + }); + + let newDefaultValue = defaultValue; + if (defaultValue && values) { + const valObj = values.find((v: any) => v.value === defaultValue); + if (valObj) newDefaultValue = valObj.label; + } else if (defaultValue) { + const child = childrenArray.find((c: any) => (c as any).props.value === defaultValue); + if (child) { + newDefaultValue = (child as any).props.label || (child as any).props.value; + } + } + + return ( + + {newChildren} + + ); +} + + diff --git a/apps/docs/components/docusaurus/themed-image.tsx b/apps/docs/components/docusaurus/themed-image.tsx new file mode 100644 index 000000000..2c86c56be --- /dev/null +++ b/apps/docs/components/docusaurus/themed-image.tsx @@ -0,0 +1,17 @@ +"use client"; +import Image from 'next/image'; +import { useTheme } from 'next-themes'; +import { useEffect, useState } from 'react'; + +export default function ThemedImage({ sources, alt, ...props }: any) { + const { resolvedTheme } = useTheme(); + const [mounted, setMounted] = useState(false); + + useEffect(() => setMounted(true), []); + + if (!mounted) return {alt}; + + const src = resolvedTheme === 'dark' ? sources.dark : sources.light; + + return {alt}; +} diff --git a/apps/docs/components/feedback.tsx b/apps/docs/components/feedback.tsx new file mode 100644 index 000000000..b2c2e586f --- /dev/null +++ b/apps/docs/components/feedback.tsx @@ -0,0 +1,56 @@ +'use client'; + +import { useState } from 'react'; +import { mixpanelClient } from '@/utils/mixpanel'; +import { usePathname } from 'next/navigation'; +import { ThumbsUp, ThumbsDown } from 'lucide-react'; +import { cn } from '@/utils/cn'; + +export function Feedback() { + const pathname = usePathname(); + const [voted, setVoted] = useState(null); + + const handleVote = (useful: boolean) => { + setVoted(useful); + mixpanelClient.track('helpfulness_vote', { + path: pathname, + useful: useful, + }); + }; + + if (voted !== null) { + return ( +
+

+ Thanks for your feedback! +

+
+ ); + } + + return ( +
+

Was this page helpful?

+
+ + +
+
+ ); +} diff --git a/docs/src/components/frameworks.jsx b/apps/docs/components/frameworks.jsx similarity index 92% rename from docs/src/components/frameworks.jsx rename to apps/docs/components/frameworks.jsx index a514b8ed2..4307a64f5 100644 --- a/docs/src/components/frameworks.jsx +++ b/apps/docs/components/frameworks.jsx @@ -1,6 +1,6 @@ import React from "react"; import { Tile } from "./tile"; -import frameworks from "../../frameworks.json"; +import frameworks from "../frameworks.json"; export function Frameworks({ filter }) { const filteredFrameworks = frameworks.filter((framework) => { diff --git a/apps/docs/components/inkeep-search.tsx b/apps/docs/components/inkeep-search.tsx new file mode 100644 index 000000000..677729cd8 --- /dev/null +++ b/apps/docs/components/inkeep-search.tsx @@ -0,0 +1,71 @@ +"use client"; + +import type { SharedProps } from "fumadocs-ui/components/dialog/search"; +import { + InkeepModalSearchAndChat, + type InkeepModalSearchAndChatProps, +} from "@inkeep/cxkit-react"; +import { useEffect, useState } from "react"; + +export default function CustomDialog(props: SharedProps) { + const [syncTarget, setSyncTarget] = useState(null); + const { open, onOpenChange } = props; + // We do this because document is not available in the server + useEffect(() => { + setSyncTarget(document.documentElement); + }, []); + + const config: InkeepModalSearchAndChatProps = { + baseSettings: { + apiKey: process.env.NEXT_PUBLIC_INKEEP_API_KEY, + primaryBrandColor: '#425b76', + organizationDisplayName: "Zitadel", + colorMode: { + sync: { + target: syncTarget, + attributes: ["class"], + isDarkMode: (attributes) => !!attributes.class?.includes("dark"), + }, + }, + }, + modalSettings: { + isOpen: open, + onOpenChange, + // optional settings + }, + searchSettings: { + // optional settings + }, + aiChatSettings: { + aiAssistantName: 'Zitadel AI Assistant', + chatSubjectName: 'Zitadel', + placeholder: 'How can I enable MFA?', + introMessage: 'Hey! I’m the Zitadel AI assistant 🤖 — throw your questions my way!', + aiAssistantAvatar: '/icons/favicon-32x32.png', + userAvatar: '/icons/user.png', + exampleQuestionsLabel: 'Example Questions:', + exampleQuestions: [ + 'What is an Instance?', + 'What is an Organization?', + 'How to create an Action?', + 'How to integrate Zitadel to my React app?', + ], + isFirstExampleQuestionHighlighted: true, + shouldOpenLinksInNewTab: true, + isCopyChatButtonVisible: true, + disclaimerSettings: { + isEnabled: true, + label: 'AI Assistant', + tooltip: 'Responses are AI-generated and may require verification.', + }, + toolbarButtonLabels: { + clear: 'Start Over', + share: 'Share Chat', + getHelp: 'Get Help', + stop: 'Stop', + copyChat: 'Copy Chat', + }, + }, + }; + return ; +} \ No newline at end of file diff --git a/apps/docs/components/mixpanel-provider.tsx b/apps/docs/components/mixpanel-provider.tsx new file mode 100644 index 000000000..fff346367 --- /dev/null +++ b/apps/docs/components/mixpanel-provider.tsx @@ -0,0 +1,51 @@ +'use client'; + +import { initMixpanel, mixpanelClient, optInTracking, optOutTracking } from '@/utils/mixpanel'; +import React, { useEffect } from 'react'; + +export default function MixpanelProvider({ children }: { children: React.ReactNode }) { + useEffect(() => { + const setup = async () => { + await initMixpanel(); + + const CookieConsent = await import('vanilla-cookieconsent'); + const cookie = CookieConsent.getCookie(); + + // @ts-ignore + if (!cookie || !cookie.services || !cookie.services.analytics) { + // We only send data when people opt-in, not just if they have not opted out. + return; + } + // @ts-ignore + const hasMixpanelConsent = cookie?.services?.analytics?.indexOf('mixpanel') !== -1; + if (hasMixpanelConsent) { + optInTracking(); + } else { + optOutTracking(); + } + }; + setup().catch((error) => { + console.error('Error setting up Mixpanel:', error); + }); + + const handleConsentChange = async () => { + const CookieConsent = await import('vanilla-cookieconsent'); + const cookie = CookieConsent.getCookie(); + // @ts-ignore + const hasMixpanelConsent = cookie?.services?.analytics?.indexOf('mixpanel') !== -1; + if (hasMixpanelConsent) { + optInTracking(); + } else { + optOutTracking(); + } + }; + + window.addEventListener('cc:onChange:mixpanel', handleConsentChange); + + return () => { + window.removeEventListener('cc:onChange:mixpanel', handleConsentChange); + }; + }, []); + + return <>{children}; +} diff --git a/docs/src/components/pii_table.jsx b/apps/docs/components/pii_table.jsx similarity index 80% rename from docs/src/components/pii_table.jsx rename to apps/docs/components/pii_table.jsx index fd3de78fb..f59a28488 100644 --- a/docs/src/components/pii_table.jsx +++ b/apps/docs/components/pii_table.jsx @@ -81,26 +81,34 @@ export function PiiTable() { ], subjects: "All users" }, - ] + ] return ( - - - - - - { - pii.map((row, rowID) => { - return ( - - - + + + + + + + + + { + pii.map((row) => ( + + + - ) - }) - } + )) + } +
Type of personal dataExamplesAffected data subjects
{row.type}
    {row.examples.map((example) => { return (
  • {example}
  • )})}
Type of personal dataExamplesAffected data subjects
{row.type} +
    + {row.examples.map((example) => ( +
  • {example}
  • + ))} +
+
{row.subjects}
); } diff --git a/apps/docs/components/plausible-provider.tsx b/apps/docs/components/plausible-provider.tsx new file mode 100644 index 000000000..ae78a22b8 --- /dev/null +++ b/apps/docs/components/plausible-provider.tsx @@ -0,0 +1,17 @@ +'use client'; + +import Script from 'next/script'; + +export default function PlausibleProvider() { + const domain = process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN || 'zitadel.com'; + + return ( +