mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 13:52:51 +00:00
## Todos for release - [x] Configure Env in docs project on vercel - [x] Configure Root Path in the docs project on vercel - [ ] Remove old CSP https://github.com/zitadel/website/pull/1592 ## What we did This pull request migrates the project documentation from the old `docs/` directory to the new `apps/docs/` directory, introduces a new documentation system built with Next.js and Fumadocs, and updates all relevant references, configuration files, and documentation to reflect this change. It also adds new configuration and ignore files for the new documentation app, updates CI and linting to exclude the new docs from certain checks, and revises the contributing guidelines accordingly. **Documentation System Migration and New Docs App** * Migrated all documentation from `docs/` to `apps/docs/`, and updated all references in `README.md`, `CONTRIBUTING.md`, and other files to point to the new location. [[1]](diffhunk://#diff-eca12c0a30e25b4b46522ebf89465a03ba72a03f540796c979137931d8f92055L585-L640) [[2]](diffhunk://#diff-eca12c0a30e25b4b46522ebf89465a03ba72a03f540796c979137931d8f92055L660-R607) [[3]](diffhunk://#diff-eca12c0a30e25b4b46522ebf89465a03ba72a03f540796c979137931d8f92055L740-R687) [[4]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5L2-R3) [[5]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5L30-R30) * Added a new Next.js/Fumadocs-based documentation app under `apps/docs/`, including core app files, layouts, routing, search API, and a comprehensive `README.md` with development and contribution instructions. [[1]](diffhunk://#diff-5a1b07344a2c1b4d3f37b23ff1388b62cd9f57dea3c1cd23d8a0412b7602b132R1-R74) [[2]](diffhunk://#diff-462b9ad1eabbb7d1c29bb9c36e4931eb180fd7190900e6d2babf8f4d66ad1c28R1-R39) [[3]](diffhunk://#diff-e16ae25660ded787b10ac35dea96d5ecaacf895dae0afc8a9bd4382dc79a8c87R1-R7) [apps/docs/app/[[...slug]]/layout.tsxR1-R81](diffhunk://#diff-59e08acde4e805b7aeccef1dcf98f1d71dfc550777e6b9402085cee0e9fa4e0aR1-R81), [apps/docs/app/[[...slug]]/page.tsxR1-R79](diffhunk://#diff-e5df3f80d0fa01e12d63d81f29c57a9d14e846c78fd3beabb2ef768e38fd9580R1-R79), [[4]](diffhunk://#diff-389b34918e040cacaa87cd7201ffa462cc2b0b716736f537e3d3c660ac69353bR1-R7) [[5]](diffhunk://#diff-d3b03416d1c457b19f1c27b26f2db741412df841b875c26ccadc36e4522247f4R1-R29) [[6]](diffhunk://#diff-c8fb8339570a5305809be7c618e14705fd86390dc278e6ce8ba224a7bc8b0c3cR1-R25) **Configuration and Tooling Updates** * Updated `.github/workflows/codeql.yml`, `.golangci.yaml`, and `.github/dependabot.yml` to properly handle the new docs app: excluded `apps/docs` from certain checks, added npm dependency updates for the docs app, and excluded generated content. [[1]](diffhunk://#diff-12783128521e452af0cfac94b99b8d250413c516ec71fe6d97dbea666ff7ba27L8-R14) [[2]](diffhunk://#diff-9917ddc9f1c3304218f7269265b746d997c5c0615478177b5fceecd33ef47cb5R5-R6) [[3]](diffhunk://#diff-9917ddc9f1c3304218f7269265b746d997c5c0615478177b5fceecd33ef47cb5R126-R129) [[4]](diffhunk://#diff-dd4fbda47e51f1e35defb9275a9cd9c212ecde0b870cba89ddaaae65c5f3cd28R89-R106) * Updated `.devcontainer/devcontainer.json` to use the latest Go 1.25.3 version for consistency. **Licensing and Miscellaneous** * Added `apps/docs/` to the list of licensed directories in `LICENSING.md`. These changes ensure the documentation is now maintained in a modern, scalable system and all project tooling is updated to support the new structure. --------- Co-authored-by: Federico Coppede <[email protected]>
242 lines
7.9 KiB
TypeScript
242 lines
7.9 KiB
TypeScript
import { generateFiles } from 'fumadocs-openapi';
|
|
import { createOpenAPI } from 'fumadocs-openapi/server';
|
|
import { writeFileSync, mkdirSync, readdirSync, lstatSync, readFileSync, existsSync } from 'fs';
|
|
import path, { join, dirname, basename } from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
import { glob } from 'glob';
|
|
import yaml from 'js-yaml';
|
|
|
|
// Suppress "Generated: ..." logs to avoid Vercel log limits
|
|
const originalLog = console.log;
|
|
console.log = (...args) => {
|
|
if (args.length > 0 && typeof args[0] === 'string' && args[0].startsWith('Generated: ')) {
|
|
return;
|
|
}
|
|
originalLog(...args);
|
|
};
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const ROOT_DIR = join(__dirname, '..');
|
|
const OPENAPI_ROOT = join(ROOT_DIR, 'openapi');
|
|
const CONTENT_ROOT = join(ROOT_DIR, 'content');
|
|
const CONTENT_VERSIONS_ROOT = join(ROOT_DIR, 'content');
|
|
|
|
async function generateVersionApiDocs(version: string) {
|
|
const sourceRoot = join(OPENAPI_ROOT, version);
|
|
if (!existsSync(sourceRoot)) return;
|
|
|
|
const outputRoot = version === 'latest'
|
|
? join(CONTENT_ROOT, 'reference/api')
|
|
: join(CONTENT_VERSIONS_ROOT, `${version}/reference/api`);
|
|
|
|
console.log(`Generating API docs for version: ${version} -> ${outputRoot}`);
|
|
mkdirSync(outputRoot, { recursive: true });
|
|
|
|
const specs = await glob('**/*.openapi.json', { cwd: sourceRoot });
|
|
const services: string[] = [];
|
|
|
|
for (const specPath of specs) {
|
|
const fullPath = join(sourceRoot, specPath);
|
|
const content = readFileSync(fullPath, 'utf8');
|
|
const doc = JSON.parse(content) as any;
|
|
|
|
if (!doc.paths || Object.keys(doc.paths).length === 0) continue;
|
|
|
|
let service = basename(specPath, '.openapi.json');
|
|
if (service.endsWith('_service')) {
|
|
service = service.slice(0, -8);
|
|
}
|
|
|
|
// For services in subdirectories (like resource/userschema),
|
|
// we want a unique but readable name.
|
|
const relDir = dirname(specPath);
|
|
const folderPrefix = relDir !== '.' && !relDir.startsWith('zitadel')
|
|
? relDir.split('/').pop() + '-'
|
|
: '';
|
|
const uniqueService = folderPrefix + service;
|
|
|
|
const outputDir = join(outputRoot, uniqueService);
|
|
services.push(uniqueService);
|
|
|
|
const api = createOpenAPI({
|
|
input: [path.relative(ROOT_DIR, fullPath)],
|
|
});
|
|
|
|
await generateFiles({
|
|
input: api,
|
|
output: outputDir,
|
|
includeDescription: true,
|
|
});
|
|
|
|
const indexPath = join(outputDir, 'index.mdx');
|
|
const title = uniqueService.split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join(' ');
|
|
const indexContent = `---
|
|
title: ${title} API
|
|
---
|
|
|
|
API Reference for ${title}
|
|
`;
|
|
writeFileSync(indexPath, indexContent);
|
|
}
|
|
|
|
// Generate meta.json
|
|
const meta = {
|
|
title: "APIs",
|
|
pages: services
|
|
.filter(s => !s.includes('beta') && !s.includes('alpha'))
|
|
.sort()
|
|
};
|
|
|
|
writeFileSync(
|
|
join(outputRoot, 'meta.json'),
|
|
JSON.stringify(meta, null, 2)
|
|
);
|
|
}
|
|
|
|
async function fixAllGeneratedLinks() {
|
|
console.log('Post-processing: Fixing API links...');
|
|
const files = await glob('**/*.{md,mdx}', { cwd: CONTENT_ROOT });
|
|
const fileIndex = new Map<string, Map<string, string>>(); // version -> (Name -> absoluteURL)
|
|
|
|
const getVersion = (filePath: string) => {
|
|
const parts = filePath.split(path.sep);
|
|
if (parts[0].startsWith('v4.')) return parts[0];
|
|
return 'latest';
|
|
};
|
|
|
|
const getUrl = (filePath: string) => {
|
|
return '/' + filePath.replace(/\.(md|mdx)$/, '').split(path.sep).join('/');
|
|
};
|
|
|
|
// Build index
|
|
for (const file of files) {
|
|
const version = getVersion(file);
|
|
if (!fileIndex.has(version)) fileIndex.set(version, new Map());
|
|
const versionMap = fileIndex.get(version)!;
|
|
|
|
const name = basename(file.replace(/\.md$/, ''), '.mdx');
|
|
const parts = name.split('.');
|
|
|
|
const operation = parts[parts.length - 1];
|
|
const service = parts.length > 1 ? parts[parts.length - 2] : null;
|
|
const url = getUrl(file);
|
|
|
|
// Prioritize non-beta/non-alpha in the index
|
|
const isBeta = name.includes('beta') || name.includes('alpha');
|
|
const existing = versionMap.get(operation.toLowerCase());
|
|
if (!existing || (!isBeta && existing.includes('beta'))) {
|
|
versionMap.set(operation.toLowerCase(), url);
|
|
}
|
|
|
|
if (service) {
|
|
const serviceOp = `${service}.${operation}`.toLowerCase();
|
|
const existingServiceOp = versionMap.get(serviceOp);
|
|
if (!existingServiceOp || (!isBeta && existingServiceOp.includes('beta'))) {
|
|
versionMap.set(serviceOp, url);
|
|
}
|
|
}
|
|
versionMap.set(name.toLowerCase(), url);
|
|
}
|
|
|
|
let fixedCount = 0;
|
|
for (const file of files) {
|
|
const fullPath = join(CONTENT_ROOT, file);
|
|
const content = readFileSync(fullPath, 'utf-8');
|
|
const version = getVersion(file);
|
|
const versionMap = fileIndex.get(version);
|
|
|
|
// [Text](apis/resources/service_name/file_name.api.mdx)
|
|
const linkRegex = /\[([^\]]+)\]\(([\/]?apis\/resources\/([^\/]+)\/([^\/)]+)\.api\.mdx)\)/g;
|
|
|
|
let modified = false;
|
|
let newContent = content.replace(linkRegex, (match, text, fullLink, serviceSlug, fileSlug) => {
|
|
const toPascalCase = (s: string) => s.split(/[-_]/).map(p => p.charAt(0).toUpperCase() + p.slice(1)).join('');
|
|
|
|
// Extraction strategy: fileSlug is "user-service-get-user-by-id"
|
|
const serviceIndex = fileSlug.lastIndexOf('service-');
|
|
const operationKebab = serviceIndex !== -1 ? fileSlug.slice(serviceIndex + 8) : fileSlug;
|
|
const operation = toPascalCase(operationKebab);
|
|
|
|
let targetUrl = versionMap?.get(operation.toLowerCase());
|
|
|
|
if (!targetUrl && serviceSlug) {
|
|
const serviceMatch = serviceSlug.match(/^([a-z_]+)_service/);
|
|
if (serviceMatch) {
|
|
const service = toPascalCase(serviceMatch[1]) + 'Service';
|
|
targetUrl = versionMap?.get(`${service}.${operation}`.toLowerCase());
|
|
}
|
|
}
|
|
|
|
if (targetUrl) {
|
|
modified = true;
|
|
fixedCount++;
|
|
return `[${text}](${targetUrl})`;
|
|
}
|
|
return match;
|
|
});
|
|
|
|
// Fix v2beta links that were likely in the source proto comments
|
|
// and also remove potential double /docs prefixing from source comments
|
|
const internalLinkRegex = /\[([^\]]+)\]\(([\/]?docs\/)?reference\/api\/([^\/]+)\/([^\s)]+)\)/g;
|
|
newContent = newContent.replace(internalLinkRegex, (match, text, docsPrefix, service, fileSlug) => {
|
|
let targetFileSlug = fileSlug;
|
|
let isV2Beta = fileSlug.includes('.v2beta.');
|
|
|
|
if (isV2Beta) {
|
|
const v2Target = fileSlug.replace('.v2beta.', '.v2.');
|
|
// Only rename to v2 if the v2 target actually exists in our index
|
|
if (versionMap?.has(v2Target.toLowerCase())) {
|
|
targetFileSlug = v2Target;
|
|
} else {
|
|
// If no v2 target, and we excluded v2beta, this link is dead.
|
|
// Return plain text instead of a broken link to satisfy the link checker.
|
|
modified = true;
|
|
return text;
|
|
}
|
|
}
|
|
|
|
const targetUrl = `/reference/api/${service}/${targetFileSlug}`;
|
|
if (docsPrefix || isV2Beta) {
|
|
modified = true;
|
|
return `[${text}](${targetUrl})`;
|
|
}
|
|
return match;
|
|
});
|
|
|
|
if (modified) {
|
|
writeFileSync(fullPath, newContent);
|
|
}
|
|
}
|
|
console.log(`Post-processing: Fixed ${fixedCount} links.`);
|
|
}
|
|
|
|
async function run() {
|
|
const args = process.argv.slice(2);
|
|
const onlyGenerate = args.includes('--only-generate');
|
|
const onlyFix = args.includes('--only-fix');
|
|
// Default to both if no specific flag is set
|
|
const runAll = !onlyGenerate && !onlyFix;
|
|
|
|
if (runAll || onlyGenerate) {
|
|
if (!existsSync(OPENAPI_ROOT)) {
|
|
console.error('OpenAPI root not found. Run generate-buf.mjs first.');
|
|
process.exit(1);
|
|
}
|
|
|
|
const versions = readdirSync(OPENAPI_ROOT).filter(f => lstatSync(join(OPENAPI_ROOT, f)).isDirectory() && f !== 'zitadel');
|
|
|
|
for (const version of versions) {
|
|
await generateVersionApiDocs(version);
|
|
}
|
|
}
|
|
|
|
if (runAll || onlyFix) {
|
|
await fixAllGeneratedLinks();
|
|
}
|
|
}
|
|
|
|
run().catch(err => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|