@readme/cli 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/git-format.js +80 -0
- package/bin/readme.js +1 -1
- package/package.json +54 -26
- package/src/bootstrap.js +9 -13
- package/src/cli.js +17 -11
- package/src/commands/components.js +35 -18
- package/src/commands/dev.js +12 -8
- package/src/commands/eyes.js +12 -8
- package/src/commands/import.js +2200 -2049
- package/src/commands/lint.js +33 -29
- package/src/commands/oas-bundle.js +6 -2
- package/src/commands/oas-sync.js +271 -161
- package/src/commands/oas-validate.js +10 -7
- package/src/commands/pretty.js +188 -165
- package/src/commands/setup.js +24 -26
- package/src/commands/versions.js +6 -2
- package/src/dev/app/[...slug]/page.js +1 -0
- package/src/dev/app/layout.js +18 -11
- package/src/dev/app/page.js +1 -0
- package/src/dev/lib/docs.js +27 -19
- package/src/dev/next.config.mjs +2 -2
- package/src/gha.js +7 -7
- package/src/prompts/index.js +16 -6
- package/src/utils/containment.js +87 -0
- package/src/utils/eyes.js +143 -51
- package/src/utils/git.js +3 -3
- package/src/utils/lint.js +39 -38
- package/src/utils/llms.js +94 -91
- package/src/utils/markdown.js +256 -0
- package/src/utils/marketplace.js +12 -5
- package/src/utils/multi-spinner.js +5 -7
- package/src/utils/promise-pool.js +7 -5
- package/src/utils/reporter.js +88 -94
- package/src/utils/setup-templates.js +12 -6
- package/src/utils/styles.js +2 -1
- package/src/utils/tamagotchi.js +257 -91
- package/src/utils/tips.js +18 -9
- package/src/utils/url-segs.js +46 -41
- package/src/validators/components.js +55 -63
- package/src/validators/content.js +16 -14
- package/src/validators/duplicates.js +6 -2
- package/src/validators/frontmatter.js +21 -29
- package/src/validators/links.js +4 -8
- package/src/validators/oas-reference.js +25 -18
- package/src/validators/oas-schema.js +2 -0
- package/src/validators/ordering.js +5 -5
- package/src/validators/recipes.js +4 -12
- package/vendor/git-format/CLAUDE.md +352 -0
- package/vendor/git-format/frontmatter.schema.json +199 -0
- package/src/commands/import-oas.test.js +0 -277
- package/src/commands/import.classify.test.js +0 -172
- package/src/commands/import.landing.test.js +0 -217
- package/src/commands/import.slug-planner.test.js +0 -86
- package/src/commands/import.staging.test.js +0 -230
- package/src/commands/import.test.js +0 -382
- package/src/utils/llms.test.js +0 -302
- package/src/utils/url-segs.test.js +0 -239
package/src/validators/links.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
import { parseFrontmatter } from '../utils/markdown.js';
|
|
4
5
|
import { slugForEntry } from '../utils/slug-for-entry.js';
|
|
5
6
|
|
|
6
7
|
export const name = 'links';
|
|
@@ -36,13 +37,8 @@ export function validateAll(files, gitRoot) {
|
|
|
36
37
|
if (!CHECKED_DIRS.includes(topDir)) continue;
|
|
37
38
|
if (!/\.(md|mdx)$/.test(relPath)) continue;
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const raw = fs.readFileSync(path.join(gitRoot, relPath), 'utf-8');
|
|
42
|
-
({ content: body } = matter(raw));
|
|
43
|
-
} catch {
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
40
|
+
const raw = fs.readFileSync(path.join(gitRoot, relPath), 'utf-8');
|
|
41
|
+
const { content: body } = parseFrontmatter(raw);
|
|
46
42
|
|
|
47
43
|
for (const match of body.matchAll(DOC_LINK_RE)) {
|
|
48
44
|
const prefix = match[0].slice(1, match[0].indexOf(':'));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
|
|
3
|
+
|
|
4
4
|
import {
|
|
5
5
|
findOasFiles,
|
|
6
6
|
extractOperations,
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
operationKey,
|
|
10
10
|
findIgnoredInternalExtensions,
|
|
11
11
|
} from '../commands/oas-sync.js';
|
|
12
|
+
import { PathEscapeError } from '../utils/containment.js';
|
|
13
|
+
import { parseFrontmatter } from '../utils/markdown.js';
|
|
12
14
|
|
|
13
15
|
export const name = 'oas-reference';
|
|
14
16
|
|
|
@@ -37,17 +39,11 @@ export function validateAll(files, gitRoot, { fix } = {}) {
|
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
// Collect all reference pages with api frontmatter.
|
|
40
|
-
const refPages = files.filter(
|
|
42
|
+
const refPages = files.filter(f => f.startsWith('reference/') && f.endsWith('.md'));
|
|
41
43
|
|
|
42
44
|
for (const relPath of refPages) {
|
|
43
45
|
const filePath = path.join(gitRoot, relPath);
|
|
44
|
-
|
|
45
|
-
try {
|
|
46
|
-
({ data } = matter(fs.readFileSync(filePath, 'utf-8')));
|
|
47
|
-
} catch {
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
|
|
46
|
+
const { data } = parseFrontmatter(fs.readFileSync(filePath, 'utf-8'));
|
|
51
47
|
if (!data.api || !data.api.file) continue;
|
|
52
48
|
|
|
53
49
|
const oasFilename = data.api.file;
|
|
@@ -85,11 +81,9 @@ export function validateAll(files, gitRoot, { fix } = {}) {
|
|
|
85
81
|
// Check for missing pages: operations in the spec with no corresponding page.
|
|
86
82
|
const existingPages = collectExistingPages(refDir);
|
|
87
83
|
for (const [oasFilename, { ops }] of oasMap) {
|
|
88
|
-
const pagesForOas = existingPages.filter(
|
|
84
|
+
const pagesForOas = existingPages.filter(p => p.data.api.file === oasFilename);
|
|
89
85
|
const coveredOps = new Set(
|
|
90
|
-
pagesForOas.map((p)
|
|
91
|
-
operationKey({ operationId: p.data.api.operationId, isWebhook: !!p.data.api.webhook }),
|
|
92
|
-
),
|
|
86
|
+
pagesForOas.map(p => operationKey({ operationId: p.data.api.operationId, isWebhook: !!p.data.api.webhook })),
|
|
93
87
|
);
|
|
94
88
|
|
|
95
89
|
for (const op of ops.values()) {
|
|
@@ -109,12 +103,25 @@ export function validateAll(files, gitRoot, { fix } = {}) {
|
|
|
109
103
|
// is actually fixable. The sync adds, deletes, moves, hides and reorders
|
|
110
104
|
// reference files, which is far too much to do on the strength of an
|
|
111
105
|
// unfixable warning (`x-readme.internal`, a page pointing at a missing spec).
|
|
112
|
-
if (fix && results.some(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
106
|
+
if (fix && results.some(r => r.fixable)) {
|
|
107
|
+
try {
|
|
108
|
+
const syncResults = syncOas(gitRoot);
|
|
109
|
+
if (syncResults) {
|
|
110
|
+
for (const r of results) {
|
|
111
|
+
if (r.fixable) r.message += ' (fixed)';
|
|
112
|
+
}
|
|
117
113
|
}
|
|
114
|
+
} catch (err) {
|
|
115
|
+
if (!(err instanceof PathEscapeError)) throw err;
|
|
116
|
+
|
|
117
|
+
// Don't mark the other fixes applied — sync stopped before it could.
|
|
118
|
+
results.push({
|
|
119
|
+
file: path.relative(gitRoot, err.target) || 'reference',
|
|
120
|
+
rule: name,
|
|
121
|
+
severity: 'error',
|
|
122
|
+
message: err.message,
|
|
123
|
+
fixable: false,
|
|
124
|
+
});
|
|
118
125
|
}
|
|
119
126
|
}
|
|
120
127
|
|
|
@@ -33,7 +33,7 @@ function parseOrderYaml(content) {
|
|
|
33
33
|
return content
|
|
34
34
|
.split('\n')
|
|
35
35
|
.map(parseOrderEntry)
|
|
36
|
-
.filter(
|
|
36
|
+
.filter(entry => entry !== null);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
function isIndex(name) {
|
|
@@ -108,7 +108,7 @@ export function validateAll(files, gitRoot, { fix } = {}) {
|
|
|
108
108
|
|
|
109
109
|
// On disk but not ordered.
|
|
110
110
|
if (requireOrder) {
|
|
111
|
-
const missing = [...onDisk].filter(
|
|
111
|
+
const missing = [...onDisk].filter(slug => !ordered.has(slug)).sort();
|
|
112
112
|
if (missing.length > 0) {
|
|
113
113
|
results.push({
|
|
114
114
|
file: relOrder,
|
|
@@ -152,7 +152,7 @@ export function validateAll(files, gitRoot, { fix } = {}) {
|
|
|
152
152
|
for (const r of results) {
|
|
153
153
|
if (!r._fixAdd) continue;
|
|
154
154
|
const { orderPath, missing } = r._fixAdd;
|
|
155
|
-
const newEntries = missing.map(
|
|
155
|
+
const newEntries = missing.map(slug => `- ${yamlSafeSlug(slug)}`).join('\n');
|
|
156
156
|
|
|
157
157
|
if (fs.existsSync(orderPath)) {
|
|
158
158
|
const existing = fs.readFileSync(orderPath, 'utf-8');
|
|
@@ -180,11 +180,11 @@ export function validateAll(files, gitRoot, { fix } = {}) {
|
|
|
180
180
|
const kept = fs
|
|
181
181
|
.readFileSync(orderPath, 'utf-8')
|
|
182
182
|
.split('\n')
|
|
183
|
-
.filter(
|
|
183
|
+
.filter(line => {
|
|
184
184
|
const entry = parseOrderEntry(line);
|
|
185
185
|
return entry === null || !toRemove.has(entry);
|
|
186
186
|
});
|
|
187
|
-
if (kept.some(
|
|
187
|
+
if (kept.some(line => parseOrderEntry(line) !== null)) {
|
|
188
188
|
fs.writeFileSync(orderPath, kept.join('\n'));
|
|
189
189
|
} else {
|
|
190
190
|
fs.unlinkSync(orderPath);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { parseFrontmatter } from '../utils/markdown.js';
|
|
2
2
|
|
|
3
3
|
export const name = 'recipes';
|
|
4
4
|
|
|
@@ -76,16 +76,8 @@ export function validate({ content, relativePath }) {
|
|
|
76
76
|
if (!relativePath.endsWith('.md')) return null;
|
|
77
77
|
|
|
78
78
|
const results = [];
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
({ data, content: body } = matter(content));
|
|
84
|
-
} catch {
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (!data.recipe) return null;
|
|
79
|
+
const { data, content: body, error } = parseFrontmatter(content);
|
|
80
|
+
if (error || !data.recipe) return null;
|
|
89
81
|
|
|
90
82
|
const codeBlocks = parseCodeBlocks(body);
|
|
91
83
|
const snippetRefs = parseSnippetRefs(body);
|
|
@@ -118,7 +110,7 @@ export function validate({ content, relativePath }) {
|
|
|
118
110
|
const block = blocksByLang.get(ref.lang);
|
|
119
111
|
|
|
120
112
|
if (!block) {
|
|
121
|
-
const available = codeBlocks.map(
|
|
113
|
+
const available = codeBlocks.map(b => b.lang).join(', ');
|
|
122
114
|
results.push({
|
|
123
115
|
file: relativePath,
|
|
124
116
|
rule: name,
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
# ReadMe Git-Based Docs Format
|
|
2
|
+
|
|
3
|
+
This repo is the reference implementation of ReadMe's Git-based documentation format. It defines how documentation is structured when syncing with ReadMe via Git.
|
|
4
|
+
|
|
5
|
+
Any LLM modifying a repo that uses this format should reference this file and the examples in this repo.
|
|
6
|
+
|
|
7
|
+
## Directory structure
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
your-repo/
|
|
11
|
+
├── frontmatter.schema.json # Frontmatter validation schema
|
|
12
|
+
├── docs/ # Guides and documentation pages
|
|
13
|
+
│ ├── {Category Name}/ # Category folders (Title Case with spaces)
|
|
14
|
+
│ │ ├── _order.yaml # Page ordering within this category
|
|
15
|
+
│ │ ├── page-slug.md # Documentation pages (kebab-case filenames)
|
|
16
|
+
│ │ └── parent-page/ # Parent page with children
|
|
17
|
+
│ │ ├── index.md # The parent page content
|
|
18
|
+
│ │ └── child-page.md # Child page nested under parent
|
|
19
|
+
│ └── {Another Category}/
|
|
20
|
+
│ └── ...
|
|
21
|
+
├── reference/ # API reference documentation
|
|
22
|
+
│ ├── oas.json # OpenAPI spec file(s)
|
|
23
|
+
│ └── {API Title}/ # Matches info.title from OAS
|
|
24
|
+
│ └── {Tag}/ # Matches OAS operation tags
|
|
25
|
+
│ ├── index.md # Optional category landing page
|
|
26
|
+
│ └── operationId.md # Endpoint page linked to OAS operation
|
|
27
|
+
├── recipes/ # Code recipe/tutorial pages
|
|
28
|
+
│ ├── _order.yaml
|
|
29
|
+
│ └── recipe-slug.md
|
|
30
|
+
├── custom_pages/ # Standalone pages (markdown or HTML)
|
|
31
|
+
│ └── page-slug.md
|
|
32
|
+
├── custom_blocks/ # Reusable content snippets and components
|
|
33
|
+
│ ├── snippet.md # Markdown snippet
|
|
34
|
+
│ └── Component.mdx # React component (single-file)
|
|
35
|
+
└── reusable_content/ # (also valid top-level directory)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Valid top-level directories:** `docs/`, `reference/`, `custom_pages/`, `recipes/`, `custom_blocks/`, `reusable_content/`
|
|
39
|
+
|
|
40
|
+
## Frontmatter
|
|
41
|
+
|
|
42
|
+
Every page uses YAML frontmatter. The full schema is in `frontmatter.schema.json`. Here's a summary by content type:
|
|
43
|
+
|
|
44
|
+
### Doc pages (`docs/`)
|
|
45
|
+
|
|
46
|
+
```yaml
|
|
47
|
+
---
|
|
48
|
+
title: Page Title # Required
|
|
49
|
+
excerpt: One-sentence summary
|
|
50
|
+
hidden: true # Hides page from navigation
|
|
51
|
+
deprecated: true # Marks page as deprecated
|
|
52
|
+
icon: star # Icon identifier
|
|
53
|
+
metadata:
|
|
54
|
+
title: SEO Title
|
|
55
|
+
description: SEO description
|
|
56
|
+
keywords: [keyword1, keyword2]
|
|
57
|
+
image: https://example.com/image.png
|
|
58
|
+
robots: noindex # "index" or "noindex"
|
|
59
|
+
next:
|
|
60
|
+
description: What's next
|
|
61
|
+
pages:
|
|
62
|
+
- slug: page-slug
|
|
63
|
+
title: Display Title
|
|
64
|
+
type: basic # basic, endpoint, changelog, custom_page
|
|
65
|
+
- title: External Link
|
|
66
|
+
type: link
|
|
67
|
+
url: https://example.com
|
|
68
|
+
link:
|
|
69
|
+
url: https://example.com # Redirect page to this URL
|
|
70
|
+
new_tab: true
|
|
71
|
+
---
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Only `title` is required. All other fields are optional — don't add fields you don't need.
|
|
75
|
+
|
|
76
|
+
### API reference pages (`reference/`)
|
|
77
|
+
|
|
78
|
+
Same fields as doc pages, plus:
|
|
79
|
+
|
|
80
|
+
```yaml
|
|
81
|
+
---
|
|
82
|
+
title: Get a Pet
|
|
83
|
+
excerpt: Fetch a specific pet by ID
|
|
84
|
+
api:
|
|
85
|
+
file: oas.json # Path to OAS file (relative to reference/)
|
|
86
|
+
operationId: getPet # Matches operationId in the OAS spec
|
|
87
|
+
webhook: false # Set true for webhook operations
|
|
88
|
+
api_config: authentication # Special page type: "authentication", "getting-started", or "my-requests"
|
|
89
|
+
recipes: # Related recipe slugs
|
|
90
|
+
- publish-all-docs-in-category
|
|
91
|
+
---
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Set `api` to `null` to detach a reference page from its API operation (makes it a plain page within `reference/`).
|
|
95
|
+
|
|
96
|
+
### Recipe pages (`recipes/`)
|
|
97
|
+
|
|
98
|
+
```yaml
|
|
99
|
+
---
|
|
100
|
+
title: Publish all Docs in Category # Required
|
|
101
|
+
description: Update each doc as published. # Optional
|
|
102
|
+
hidden: true
|
|
103
|
+
recipe: # Required
|
|
104
|
+
color: '#018FF4' # Hex color, required
|
|
105
|
+
icon: circle-play # Icon string, required
|
|
106
|
+
---
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Custom pages (`custom_pages/`)
|
|
110
|
+
|
|
111
|
+
```yaml
|
|
112
|
+
---
|
|
113
|
+
title: My Custom Page # Required
|
|
114
|
+
hidden: true
|
|
115
|
+
fullscreen: true # Renders page without sidebar/nav chrome
|
|
116
|
+
metadata:
|
|
117
|
+
description: ...
|
|
118
|
+
keywords: ...
|
|
119
|
+
title: ...
|
|
120
|
+
image: ...
|
|
121
|
+
---
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Custom pages support markdown (`.md`) and HTML (`.html`). They cannot use `deprecated`.
|
|
125
|
+
|
|
126
|
+
### Custom blocks (`custom_blocks/`)
|
|
127
|
+
|
|
128
|
+
Markdown snippets (`.md`):
|
|
129
|
+
```yaml
|
|
130
|
+
---
|
|
131
|
+
name: my-snippet # Required
|
|
132
|
+
---
|
|
133
|
+
Content here...
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
React components (`.mdx`) — export a component and include a usage example:
|
|
137
|
+
```yaml
|
|
138
|
+
---
|
|
139
|
+
name: MyComponent # Required
|
|
140
|
+
---
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Ordering with `_order.yaml`
|
|
144
|
+
|
|
145
|
+
Every directory containing pages needs an `_order.yaml` to control display order.
|
|
146
|
+
|
|
147
|
+
**Category ordering** — lists folder names (Title Case with spaces):
|
|
148
|
+
```yaml
|
|
149
|
+
# docs/_order.yaml (if it existed — currently categories display alphabetically without one)
|
|
150
|
+
- Documentation
|
|
151
|
+
- More Documentation
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Page ordering within a category** — lists page slugs (kebab-case, no `.md`):
|
|
155
|
+
```yaml
|
|
156
|
+
# docs/Documentation/_order.yaml
|
|
157
|
+
- hello-world
|
|
158
|
+
- parent-page
|
|
159
|
+
- another-page
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**Recipe ordering:**
|
|
163
|
+
```yaml
|
|
164
|
+
# recipes/_order.yaml
|
|
165
|
+
- publish-all-docs-in-category
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Parent-child pages
|
|
169
|
+
|
|
170
|
+
Create a folder named after the parent page slug, with `index.md` as the parent and additional `.md` files as children:
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
docs/Documentation/
|
|
174
|
+
└── parent-page/
|
|
175
|
+
├── index.md # Parent page (title: "Parent Page")
|
|
176
|
+
└── child-page.md # Child page nested underneath
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Linking between pages
|
|
180
|
+
|
|
181
|
+
### Published URL structure
|
|
182
|
+
|
|
183
|
+
On the published ReadMe site, pages have flat URL paths based on their content type:
|
|
184
|
+
|
|
185
|
+
| Content type | Published URL |
|
|
186
|
+
|---|---|
|
|
187
|
+
| Doc pages (`docs/`) | `/docs/{slug}` |
|
|
188
|
+
| API reference (`reference/`) | `/reference/{slug}` |
|
|
189
|
+
| Recipes (`recipes/`) | `/recipes/{slug}` |
|
|
190
|
+
| Custom pages (`custom_pages/`) | `/page/{slug}` |
|
|
191
|
+
|
|
192
|
+
The slug comes from the filename (without `.md`). Category folders do **not** appear in published URLs — `docs/Getting Started/quickstart.md` becomes `/docs/quickstart`. Child pages nest under their parent: `docs/Category/parent-page/child-page.md` becomes `/docs/parent-page/child-page`.
|
|
193
|
+
|
|
194
|
+
**Important:** Site URLs may include a prefix (e.g., `/lang-en/docs/slug`), so never use absolute paths when linking between pages. Use relative links instead.
|
|
195
|
+
|
|
196
|
+
### Relative links (preferred)
|
|
197
|
+
|
|
198
|
+
Always use relative links based on the published URL structure above (not the file structure in this repo). Do not include `.md` extensions in links.
|
|
199
|
+
|
|
200
|
+
```markdown
|
|
201
|
+
<!-- Link to a page in the same section -->
|
|
202
|
+
[Link text](./other-page)
|
|
203
|
+
|
|
204
|
+
<!-- Link from a doc page to a reference page -->
|
|
205
|
+
[Link text](../reference/some-endpoint)
|
|
206
|
+
|
|
207
|
+
<!-- Link from a child page to a sibling -->
|
|
208
|
+
[Link text](./sibling-page)
|
|
209
|
+
|
|
210
|
+
<!-- Link from a child page up to another doc -->
|
|
211
|
+
[Link text](../other-page)
|
|
212
|
+
|
|
213
|
+
<!-- HTML anchor tags also work with relative links -->
|
|
214
|
+
<a href="./other-page">Link text</a>
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### `doc:` protocol links
|
|
218
|
+
|
|
219
|
+
ReadMe supports a special `doc:slug` protocol that resolves to doc pages regardless of URL prefix. This is convenient but only works in markdown links (not in `<a>` tags or component `href` attributes):
|
|
220
|
+
|
|
221
|
+
```markdown
|
|
222
|
+
[Link text](doc:hello-world)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**Use relative links as the default.** The `doc:` protocol is an acceptable alternative for markdown-only links to doc pages.
|
|
226
|
+
|
|
227
|
+
## Markdown content conventions
|
|
228
|
+
|
|
229
|
+
- Write in active voice, address the reader as "you"
|
|
230
|
+
- Use sentence case for headings
|
|
231
|
+
- Category folder names: Title Case with spaces (e.g., `Getting Started`, `Core Features`)
|
|
232
|
+
- Page filenames: lowercase kebab-case (e.g., `quickstart.md`, `error-handling.md`)
|
|
233
|
+
|
|
234
|
+
### MDX components
|
|
235
|
+
|
|
236
|
+
These built-in components are available in markdown content:
|
|
237
|
+
|
|
238
|
+
**Tabs** — for non-code tabbed content:
|
|
239
|
+
```markdown
|
|
240
|
+
<Tabs>
|
|
241
|
+
<Tab title="First Tab">
|
|
242
|
+
Content for tab 1
|
|
243
|
+
</Tab>
|
|
244
|
+
<Tab title="Second Tab">
|
|
245
|
+
Content for tab 2
|
|
246
|
+
</Tab>
|
|
247
|
+
</Tabs>
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**Tabbed code blocks** — place fenced code blocks back-to-back with NO blank lines between them:
|
|
251
|
+
|
|
252
|
+
````markdown
|
|
253
|
+
```javascript
|
|
254
|
+
const pet = await sdk.getPet({ petId: 123 });
|
|
255
|
+
```
|
|
256
|
+
```python
|
|
257
|
+
pet = sdk.get_pet(pet_id=123)
|
|
258
|
+
```
|
|
259
|
+
````
|
|
260
|
+
|
|
261
|
+
**Accordion:**
|
|
262
|
+
```markdown
|
|
263
|
+
<Accordion title="Click to expand" icon="info">
|
|
264
|
+
Hidden content here
|
|
265
|
+
</Accordion>
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**Cards:**
|
|
269
|
+
```markdown
|
|
270
|
+
<Cards columns={3}>
|
|
271
|
+
<Card title="Getting Started" icon="rocket" href="./getting-started">
|
|
272
|
+
Quick intro to the platform
|
|
273
|
+
</Card>
|
|
274
|
+
<Card title="API Reference" icon="code" href="../reference/">
|
|
275
|
+
Explore the API
|
|
276
|
+
</Card>
|
|
277
|
+
</Cards>
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**Columns:**
|
|
281
|
+
```markdown
|
|
282
|
+
<Columns>
|
|
283
|
+
<Column>
|
|
284
|
+
Left side content
|
|
285
|
+
</Column>
|
|
286
|
+
<Column>
|
|
287
|
+
Right side content
|
|
288
|
+
</Column>
|
|
289
|
+
</Columns>
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Only these components are valid. Convert any other components (e.g., `<Callout>`, `<Note>`, `<Info>`, `<Steps>`) to plain markdown or blockquotes.
|
|
293
|
+
|
|
294
|
+
### Recipe line highlighting
|
|
295
|
+
|
|
296
|
+
Recipes can highlight specific code lines per language using HTML comments:
|
|
297
|
+
|
|
298
|
+
```markdown
|
|
299
|
+
<!-- node@L0-L4,L6 -->
|
|
300
|
+
<!-- python@L2-L3,L9 -->
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
These comments go after a markdown heading and before the explanatory text for that step. They reference the code blocks at the top of the recipe.
|
|
304
|
+
|
|
305
|
+
## API reference structure
|
|
306
|
+
|
|
307
|
+
OAS (OpenAPI) spec files go in `reference/`. The directory structure maps to the API:
|
|
308
|
+
|
|
309
|
+
```
|
|
310
|
+
reference/
|
|
311
|
+
├── oas.json # The OpenAPI spec
|
|
312
|
+
└── Simple Pet API/ # Matches spec's info.title
|
|
313
|
+
├── Pet/ # Matches a tag name
|
|
314
|
+
│ ├── index.md # Optional: landing page for this tag
|
|
315
|
+
│ └── getPet.md # Endpoint page (filename = operationId)
|
|
316
|
+
└── Pets/
|
|
317
|
+
├── index.md
|
|
318
|
+
└── randomPet.md
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
If you only need the auto-generated endpoint pages (no custom markdown), you can just have the OAS file with no subdirectories — ReadMe creates "virtual" categories from the spec's tags.
|
|
322
|
+
|
|
323
|
+
Endpoint pages link to their OAS operation via frontmatter:
|
|
324
|
+
```yaml
|
|
325
|
+
api:
|
|
326
|
+
file: oas.json
|
|
327
|
+
operationId: getPet
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
You can also add **conceptual guide pages** inside `reference/` (authentication, error handling, pagination, etc.) by creating pages without an `api` frontmatter field, or by setting `api: null`.
|
|
331
|
+
|
|
332
|
+
## Validation rules
|
|
333
|
+
|
|
334
|
+
When attempting to validate the repo, these are the common issues to check for and how to fix them:
|
|
335
|
+
|
|
336
|
+
- **frontmatter** — Invalid or unknown frontmatter properties. Unknown properties should either be removed or prefixed with `x-` (e.g., `category` → `x-category`). Typos are flagged with the closest valid property name.
|
|
337
|
+
- **ordering** — Every file and subdirectory should be listed in its directory's `_order.yaml`. Slugs that YAML interprets as non-strings (numbers like `404`, booleans like `true`/`false`/`yes`/`no`) must be quoted (e.g., `- "404"`). Never remove quotes from existing entries.
|
|
338
|
+
- **numbering** — Files ending in `-1`, `-2`, etc. where no base name exists should be renamed (e.g., `setup-1.md` → `setup.md`). These suffixes are holdovers from an old bug.
|
|
339
|
+
- **duplicates** — Slugs must be unique across all `docs/` and `reference/` pages. Never rename files inside `ReadMeConfig/` — these have special internal names.
|
|
340
|
+
- **oas-reference** — Reference pages with `api.file` frontmatter must point to a valid OAS file. The `api.operationId` must exist in the spec.
|
|
341
|
+
- **oas-schema** — OAS files must be valid OpenAPI or Swagger specs. Error messages include line numbers and context.
|
|
342
|
+
- **components** — MDX component tags (`<Name>`) must reference a built-in or custom block. Custom block `.md` snippets should use PascalCase filenames (e.g., `MySnippet.md` for `<MySnippet />`).
|
|
343
|
+
- **recipes** — Must have code blocks at the top, then `# Heading` sections. Snippet references (`<!-- lang@lines -->`) must reference existing code blocks with valid line ranges.
|
|
344
|
+
|
|
345
|
+
## Key rules when editing
|
|
346
|
+
|
|
347
|
+
1. **Don't add frontmatter fields not in the schema.** Check `frontmatter.schema.json` if unsure.
|
|
348
|
+
2. **Every directory with pages needs `_order.yaml`** to control ordering.
|
|
349
|
+
3. **Category folders use Title Case with spaces.** Page files use kebab-case.
|
|
350
|
+
4. **Don't modify auto-generated endpoint files** in `reference/` unless adding custom markdown to existing ones. You can add new conceptual pages alongside them.
|
|
351
|
+
5. **Prefer more focused pages over fewer long ones.** Split distinct concepts into separate files.
|
|
352
|
+
6. **Don't create changelog/release-notes content** — ReadMe handles that separately.
|