Skip to content

Commit 7fc12f5

Browse files
gregbergeclaude
andcommitted
Make content commands aware of .gitbook.yaml
When the repository declares a .gitbook.yaml, the content commands interpret it like Git Sync does: - lint/format default to the configured content root, so files outside it (e.g. the GitHub-facing README.md) are not treated as GitBook content, and skip the configured structure.summary file instead of a hardcoded SUMMARY.md. - broken-links resolves pages and root-relative links against the configured root, and keeps checking the table of contents. Repositories without a .gitbook.yaml behave as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 8689a05 commit 7fc12f5

1 file changed

Lines changed: 58 additions & 13 deletions

File tree

packages/cli/src/content.ts

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,41 @@
11
import * as fs from 'fs/promises';
22
import * as path from 'path';
3+
import * as yaml from 'js-yaml';
34

45
import type { ContentDiagnostic } from '@gitbook/content-engine';
56

67
const IGNORED_DIRECTORIES = new Set(['node_modules', '.git']);
78

9+
interface GitBookRepoConfig {
10+
/** Absolute path of the GitBook content root. */
11+
root: string;
12+
/** File name of the table of contents (navigation, not a page). */
13+
summaryName: string;
14+
}
15+
16+
/**
17+
* Read the repository's .gitbook.yaml, when present: it defines the content
18+
* root and the structure files, exactly as Git Sync interprets them.
19+
*/
20+
async function loadGitBookRepoConfig(cwd: string): Promise<GitBookRepoConfig | null> {
21+
for (const name of ['.gitbook.yaml', '.gitbook.yml']) {
22+
try {
23+
const raw = await fs.readFile(path.join(cwd, name), 'utf8');
24+
const parsed = yaml.load(raw) as {
25+
root?: string;
26+
structure?: { summary?: string };
27+
} | null;
28+
return {
29+
root: path.resolve(cwd, parsed?.root ?? '.'),
30+
summaryName: path.basename(parsed?.structure?.summary ?? 'SUMMARY.md'),
31+
};
32+
} catch {
33+
// Try the next name.
34+
}
35+
}
36+
return null;
37+
}
38+
839
const DIFF_PREVIEW_LINES = 12;
940

1041
const useColor = process.stdout.isTTY === true;
@@ -24,12 +55,13 @@ async function loadEngine() {
2455

2556
/**
2657
* Expand a list of files and directories into the markdown files they contain.
27-
* `excludeSummary` skips SUMMARY.md files: GitBook parses them as navigation,
28-
* not as pages, so page-level lint/format do not apply to them.
58+
* `excludeSummaryName` skips the table of contents file (SUMMARY.md by
59+
* default): GitBook parses it as navigation, not as a page, so page-level
60+
* lint/format do not apply to it. Explicitly passed files are always kept.
2961
*/
3062
async function collectMarkdownFiles(
3163
inputs: string[],
32-
options: { excludeSummary?: boolean } = {},
64+
options: { excludeSummaryName?: string } = {},
3365
): Promise<string[]> {
3466
const files: string[] = [];
3567

@@ -45,7 +77,7 @@ async function collectMarkdownFiles(
4577
await visit(path.join(input, entry.name));
4678
} else if (
4779
entry.name.endsWith('.md') &&
48-
!(options.excludeSummary && entry.name === 'SUMMARY.md')
80+
entry.name !== options.excludeSummaryName
4981
) {
5082
files.push(path.join(input, entry.name));
5183
}
@@ -62,6 +94,20 @@ async function collectMarkdownFiles(
6294
return files.sort();
6395
}
6496

97+
/**
98+
* Resolve the files to check for page-level commands (lint, format): when no
99+
* input is given and the repository has a .gitbook.yaml, the configured
100+
* content root is used, so files outside it (e.g. the GitHub-facing README)
101+
* are not treated as GitBook content.
102+
*/
103+
async function collectPageFiles(inputs: string[]): Promise<string[]> {
104+
const config = await loadGitBookRepoConfig(process.cwd());
105+
const defaults = config ? [config.root] : ['.'];
106+
return await collectMarkdownFiles(inputs.length > 0 ? inputs : defaults, {
107+
excludeSummaryName: config?.summaryName ?? 'SUMMARY.md',
108+
});
109+
}
110+
65111
function printDiff(diagnostic: ContentDiagnostic) {
66112
const lines: string[] = [];
67113
if (diagnostic.actual) {
@@ -103,9 +149,7 @@ export async function lintContentFiles(
103149
): Promise<number> {
104150
const { lintContent } = await loadEngine();
105151

106-
const files = await collectMarkdownFiles(inputs.length > 0 ? inputs : ['.'], {
107-
excludeSummary: true,
108-
});
152+
const files = await collectPageFiles(inputs);
109153
if (files.length === 0) {
110154
console.log('No markdown files found.');
111155
return 0;
@@ -145,9 +189,12 @@ export async function brokenLinksContentFiles(
145189
): Promise<number> {
146190
const { checkBrokenLinks } = await loadEngine();
147191

148-
const root = process.cwd();
149-
// SUMMARY.md is included here: it is the navigation file, its links matter.
150-
const files = await collectMarkdownFiles(inputs.length > 0 ? inputs : ['.']);
192+
// Links resolve against the GitBook content root when the repository
193+
// declares one. The table of contents is included: it is the navigation
194+
// file, its links matter.
195+
const config = await loadGitBookRepoConfig(process.cwd());
196+
const root = config?.root ?? process.cwd();
197+
const files = await collectMarkdownFiles(inputs.length > 0 ? inputs : [root]);
151198
if (files.length === 0) {
152199
console.log('No markdown files found.');
153200
return 0;
@@ -203,9 +250,7 @@ export async function formatContentFiles(
203250
): Promise<number> {
204251
const { formatContent } = await loadEngine();
205252

206-
const files = await collectMarkdownFiles(inputs.length > 0 ? inputs : ['.'], {
207-
excludeSummary: true,
208-
});
253+
const files = await collectPageFiles(inputs);
209254
if (files.length === 0) {
210255
console.log('No markdown files found.');
211256
return 0;

0 commit comments

Comments
 (0)