feat: add a cli product for the gt documentation

gt, the Gitea CLI, gets its own product next to the runner: the CLI entry
in the header, its own sidebars, search scope, crawler start url and
redirects, and `make cut-version PRODUCT=cli`. Until a release series is
listed, the develop docs are served at /cli/ without the unreleased banner.

A directory with an index page and no _category_.json now takes its
sidebar label and position from that page, which leads the group, so the
generated reference can be grouped by command.

Signed-off-by: bircni <[email protected]>
This commit is contained in:
bircni committed 2026-10-22 11:28:15 +02:00
1 parent 49d912f7fb
commit fe618c42be
10 files changed
+104 -41

No files matched your search

+45 -2
View File
@@ -8,7 +8,7 @@
export type LocaleId = 'en-us' | 'zh-cn' | 'zh-tw';
export type ProductId = 'docs' | 'api' | 'runner' | 'enterprise';
export type ProductId = 'docs' | 'api' | 'runner' | 'cli' | 'enterprise';
/** Values substituted for `@name@` placeholders in the markdown sources. */
export interface VersionVariables {
@@ -246,6 +246,43 @@ const runnerProduct: ProductDef = {
],
};
/**
* gt, the Gitea CLI: one directory per release series, english only, like the
* runner. The label is the newest release of the series, which is also the tag
* the generated reference pages of that directory come from.
*
* Until the first series is listed here the develop docs are served at the
* product root, without the unreleased banner, since there is no release to
* point to. Listing a series moves them to `/cli/develop/`.
*/
const cliSeries: { id: string; latestRelease: string }[] = [];
const cliProduct: ProductDef = {
id: 'cli',
label: 'CLI',
base: 'cli',
locales: ['en-us'],
versions: [
{
id: 'develop',
label: 'develop',
sources: { 'en-us': 'cli-docs' },
...(cliSeries.length > 0
? { path: 'develop', banner: 'unreleased' as const }
: { path: '', latest: true }),
},
...cliSeries.map(
({ id, latestRelease }, index): VersionDef => ({
id,
label: latestRelease,
path: index === 0 ? '' : id,
latest: index === 0,
sources: { 'en-us': `cli-docs_versioned_docs/version-${id}` },
}),
),
],
};
/** API reference, rendered from the swagger documents by starlight-openapi. */
const apiProduct: ProductDef = {
id: 'api',
@@ -301,7 +338,13 @@ const enterpriseProduct: ProductDef = {
externalBaseUrl: 'https://docs.gitea.com/enterprise',
};
export const products: ProductDef[] = [docsProduct, apiProduct, runnerProduct, enterpriseProduct];
export const products: ProductDef[] = [
docsProduct,
apiProduct,
runnerProduct,
cliProduct,
enterpriseProduct,
];
export function getProduct(id: ProductId): ProductDef {
const product = products.find((candidate) => candidate.id === id);
+17 -8
View File
@@ -96,14 +96,23 @@ interface Tree {
label(dir: string): string;
}
/** Sidebar of a directory tree, ordered by `sidebar_position`. */
/**
* Sidebar of a directory tree, ordered by `sidebar_position`. A directory
* without `_category_.json` but with an `index` page, as the generated cli
* reference has one per command, takes its label and position from that page,
* which comes first in it.
*/
function treeOf(pages: SidebarPage[]): Tree {
const categories = new Map<string, SidebarPage>();
const indexes = new Map<string, SidebarPage>();
for (const page of pages) {
if (!page.meta.category) continue;
const dir = [page.meta.dir, page.meta.name].filter(Boolean).join('/');
categories.set(dir, page);
if (page.meta.category) {
categories.set([page.meta.dir, page.meta.name].filter(Boolean).join('/'), page);
} else if (page.meta.name === 'index' && page.meta.dir) {
indexes.set(page.meta.dir, page);
}
}
const heading = (dir: string) => categories.get(dir) ?? indexes.get(dir);
const childDirs = (dir: string) => {
const prefix = dir ? `${dir}/` : '';
@@ -121,7 +130,7 @@ function treeOf(pages: SidebarPage[]): Tree {
const entries: { sort: [number, number, string]; entry: SidebarEntry }[] = [];
for (const child of childDirs(dir)) {
const category = categories.get(child);
const category = heading(child);
const label = category?.title ?? labelFromDirname(child.split('/').at(-1)!);
entries.push({
sort: [category?.meta.order ?? last, 0, label],
@@ -131,7 +140,8 @@ function treeOf(pages: SidebarPage[]): Tree {
for (const page of pages) {
if (page.meta.dir !== dir || page.meta.category || page.hidden) continue;
entries.push({ sort: [page.meta.order ?? last, 1, page.title], entry: link(page) });
const order = indexes.get(dir) === page ? -last : (page.meta.order ?? last);
entries.push({ sort: [order, 1, page.title], entry: link(page) });
}
return entries
@@ -142,8 +152,7 @@ function treeOf(pages: SidebarPage[]): Tree {
.map((item) => item.entry);
};
const label = (dir: string) =>
categories.get(dir)?.title ?? labelFromDirname(dir.split('/').at(-1)!);
const label = (dir: string) => heading(dir)?.title ?? labelFromDirname(dir.split('/').at(-1)!);
return { build, label };
}