mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-13 21:01:21 +08:00
<!-- Make sure you've read the CONTRIBUTING.md guidelines: https://github.com/stack-auth/stack-auth/blob/dev/CONTRIBUTING.md --> Removes Platform selection, moves docs to single /content folder and no longer gens docs. Only API docs are generated here. <!-- RECURSEML_SUMMARY:START --> ## High-level PR Summary This PR makes significant changes to the documentation structure by removing platform-specific content organization and consolidating docs into a single `/content` folder. The primary goal is to simplify the documentation architecture by eliminating the platform-specific routing (Next.js, React, JavaScript, Python) and instead organizing content by topic (guides, SDK, components) regardless of platform. The PR removes platform selection functionality, platform-specific navigation, and the automatic generation of platform-specific documentation pages. It introduces a new docs tree filtering system that organizes content by section rather than by platform. These changes should make the documentation more maintainable and easier to navigate while focusing on the content itself rather than platform-specific variations. ⏱️ Estimated Review Time: 30-90 minutes <details> <summary>💡 Review Order Suggestion</summary> | Order | File Path | |-------|-----------| | 1 | `docs/package.json` | | 2 | `docs/src/lib/docs-tree.ts` | | 3 | `docs/src/lib/navigation-utils.ts` | | 4 | `docs/src/components/homepage/iconHover.tsx` | | 5 | `docs/src/components/sdk/overview.tsx` | | 6 | `docs/src/components/layouts/shared/section-utils.ts` | | 7 | `docs/src/components/layout/custom-search-dialog.tsx` | | 8 | `docs/src/app/api/search/route.ts` | | 9 | `docs/src/app/docs/[[...slug]]/page.tsx` | | 10 | `docs/src/components/layouts/docs-header-wrapper.tsx` | | 11 | `docs/src/components/layouts/docs-layout-router.tsx` | | 12 | `docs/src/components/layouts/docs.tsx` | | 13 | `package.json` | </details> [](https://discord.gg/n3SsVDAW6U) <!-- RECURSEML_SUMMARY:END --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Documentation** * Added many new guides (auth providers, OAuth, JWT, API keys, emails, webhooks, orgs/teams, permissions, onboarding, customization), expanded SDK & component reference pages, examples, and navigation metadata. * Switched docs to a simpler section-based, platform-agnostic structure and improved getting-started and production checklists. * **Developer Experience** * Enhanced docs UX: improved code-example UI with platform/framework selectors, theme-aware highlighted code blocks, image zoom, and a centralized code-sample registry. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Konstantin Wohlwend <n2d4xc@gmail.com>
173 lines
4.0 KiB
TypeScript
173 lines
4.0 KiB
TypeScript
import { throwErr } from '@stackframe/stack-shared/dist/utils/errors';
|
|
import type { PageTree } from 'fumadocs-core/server';
|
|
|
|
export type DocsSection = 'guides' | 'sdk' | 'components';
|
|
|
|
const DOCS_ROOT = '/docs';
|
|
|
|
export function resolveDocsSection(pathname: string): DocsSection {
|
|
if (pathname.startsWith(`${DOCS_ROOT}/sdk`)) {
|
|
return 'sdk';
|
|
}
|
|
|
|
if (pathname.startsWith(`${DOCS_ROOT}/components`)) {
|
|
return 'components';
|
|
}
|
|
|
|
return 'guides';
|
|
}
|
|
|
|
export function filterTreeForSection(tree: PageTree.Root, section: DocsSection): PageTree.Root {
|
|
const filteredChildren = flattenRootChildren(
|
|
pruneSeparators(
|
|
tree.children
|
|
.map(child => filterNode(child, section))
|
|
.filter((node): node is PageTree.Node => node !== null),
|
|
),
|
|
);
|
|
|
|
return {
|
|
...tree,
|
|
children: filteredChildren,
|
|
};
|
|
}
|
|
|
|
function filterNode(node: PageTree.Node, section: DocsSection): PageTree.Node | null {
|
|
if (node.type === 'separator') {
|
|
return node;
|
|
}
|
|
|
|
if (node.type === 'page') {
|
|
return matchesSection(node.url, section) ? node : null;
|
|
}
|
|
|
|
const {
|
|
index: originalIndex,
|
|
children: originalChildren,
|
|
...rest
|
|
} = node;
|
|
|
|
const filteredChildren = pruneSeparators(
|
|
originalChildren
|
|
.map(child => filterNode(child, section))
|
|
.filter((child): child is PageTree.Node => child !== null),
|
|
);
|
|
|
|
const filteredIndex = originalIndex && matchesSection(originalIndex.url, section)
|
|
? originalIndex
|
|
: undefined;
|
|
|
|
if (filteredChildren.length === 0 && !filteredIndex) {
|
|
return null;
|
|
}
|
|
|
|
const folder: PageTree.Folder = {
|
|
...rest,
|
|
type: 'folder',
|
|
children: filteredChildren,
|
|
} as PageTree.Folder;
|
|
|
|
if (filteredIndex) {
|
|
folder.index = filteredIndex;
|
|
}
|
|
|
|
return folder;
|
|
}
|
|
|
|
function matchesSection(url: string, section: DocsSection): boolean {
|
|
const cleaned = normalizeUrl(url);
|
|
|
|
if (!cleaned.startsWith(DOCS_ROOT)) {
|
|
return false;
|
|
}
|
|
|
|
if (section === 'sdk') {
|
|
return cleaned === `${DOCS_ROOT}/sdk` || cleaned.startsWith(`${DOCS_ROOT}/sdk/`);
|
|
}
|
|
|
|
if (section === 'components') {
|
|
return cleaned === `${DOCS_ROOT}/components` || cleaned.startsWith(`${DOCS_ROOT}/components/`);
|
|
}
|
|
|
|
if (cleaned.startsWith(`${DOCS_ROOT}/sdk`)) {
|
|
return false;
|
|
}
|
|
|
|
if (cleaned.startsWith(`${DOCS_ROOT}/components`)) {
|
|
return false;
|
|
}
|
|
|
|
return cleaned === DOCS_ROOT || cleaned.startsWith(`${DOCS_ROOT}/`);
|
|
}
|
|
|
|
function normalizeUrl(url: string): string {
|
|
const withoutFragment = url.split('#')[0] ?? throwErr("URL split by # returned empty array", { url });
|
|
return withoutFragment.replace(/\/$/, '');
|
|
}
|
|
|
|
function pruneSeparators(nodes: PageTree.Node[]): PageTree.Node[] {
|
|
if (nodes.length === 0) {
|
|
return nodes;
|
|
}
|
|
|
|
let start = 0;
|
|
while (start < nodes.length && nodes[start]?.type === 'separator') {
|
|
start += 1;
|
|
}
|
|
|
|
let end = nodes.length - 1;
|
|
while (end >= start && nodes[end]?.type === 'separator') {
|
|
end -= 1;
|
|
}
|
|
|
|
if (start > end) {
|
|
return [];
|
|
}
|
|
|
|
const result: PageTree.Node[] = [];
|
|
for (let i = start; i <= end; i += 1) {
|
|
const current = nodes[i];
|
|
|
|
if (current.type === 'separator' && result[result.length - 1]?.type === 'separator') {
|
|
continue;
|
|
}
|
|
|
|
result.push(current);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function flattenRootChildren(nodes: PageTree.Node[]): PageTree.Node[] {
|
|
if (nodes.length !== 1) {
|
|
return nodes;
|
|
}
|
|
|
|
const soleNode = nodes[0] ?? throwErr("Expected at least one node but array is empty", { nodesLength: nodes.length });
|
|
if (soleNode.type !== 'folder') {
|
|
return nodes;
|
|
}
|
|
|
|
const flattened: PageTree.Node[] = [];
|
|
const seenUrls = new Set<string>();
|
|
|
|
if (soleNode.index) {
|
|
const normalized = normalizeUrl(soleNode.index.url);
|
|
seenUrls.add(normalized);
|
|
flattened.push(soleNode.index);
|
|
}
|
|
|
|
soleNode.children.forEach(child => {
|
|
if (child.type === 'page') {
|
|
const normalized = normalizeUrl(child.url);
|
|
if (seenUrls.has(normalized)) {
|
|
return;
|
|
}
|
|
seenUrls.add(normalized);
|
|
}
|
|
flattened.push(child);
|
|
});
|
|
|
|
return pruneSeparators(flattened);
|
|
}
|