@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.
Files changed (57) hide show
  1. package/bin/git-format.js +80 -0
  2. package/bin/readme.js +1 -1
  3. package/package.json +54 -26
  4. package/src/bootstrap.js +9 -13
  5. package/src/cli.js +17 -11
  6. package/src/commands/components.js +35 -18
  7. package/src/commands/dev.js +12 -8
  8. package/src/commands/eyes.js +12 -8
  9. package/src/commands/import.js +2200 -2049
  10. package/src/commands/lint.js +33 -29
  11. package/src/commands/oas-bundle.js +6 -2
  12. package/src/commands/oas-sync.js +271 -161
  13. package/src/commands/oas-validate.js +10 -7
  14. package/src/commands/pretty.js +188 -165
  15. package/src/commands/setup.js +24 -26
  16. package/src/commands/versions.js +6 -2
  17. package/src/dev/app/[...slug]/page.js +1 -0
  18. package/src/dev/app/layout.js +18 -11
  19. package/src/dev/app/page.js +1 -0
  20. package/src/dev/lib/docs.js +27 -19
  21. package/src/dev/next.config.mjs +2 -2
  22. package/src/gha.js +7 -7
  23. package/src/prompts/index.js +16 -6
  24. package/src/utils/containment.js +87 -0
  25. package/src/utils/eyes.js +143 -51
  26. package/src/utils/git.js +3 -3
  27. package/src/utils/lint.js +39 -38
  28. package/src/utils/llms.js +94 -91
  29. package/src/utils/markdown.js +256 -0
  30. package/src/utils/marketplace.js +12 -5
  31. package/src/utils/multi-spinner.js +5 -7
  32. package/src/utils/promise-pool.js +7 -5
  33. package/src/utils/reporter.js +88 -94
  34. package/src/utils/setup-templates.js +12 -6
  35. package/src/utils/styles.js +2 -1
  36. package/src/utils/tamagotchi.js +257 -91
  37. package/src/utils/tips.js +18 -9
  38. package/src/utils/url-segs.js +46 -41
  39. package/src/validators/components.js +55 -63
  40. package/src/validators/content.js +16 -14
  41. package/src/validators/duplicates.js +6 -2
  42. package/src/validators/frontmatter.js +21 -29
  43. package/src/validators/links.js +4 -8
  44. package/src/validators/oas-reference.js +25 -18
  45. package/src/validators/oas-schema.js +2 -0
  46. package/src/validators/ordering.js +5 -5
  47. package/src/validators/recipes.js +4 -12
  48. package/vendor/git-format/CLAUDE.md +352 -0
  49. package/vendor/git-format/frontmatter.schema.json +199 -0
  50. package/src/commands/import-oas.test.js +0 -277
  51. package/src/commands/import.classify.test.js +0 -172
  52. package/src/commands/import.landing.test.js +0 -217
  53. package/src/commands/import.slug-planner.test.js +0 -86
  54. package/src/commands/import.staging.test.js +0 -230
  55. package/src/commands/import.test.js +0 -382
  56. package/src/utils/llms.test.js +0 -302
  57. package/src/utils/url-segs.test.js +0 -239
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import matter from 'gray-matter';
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
- let body;
40
- try {
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
- import matter from 'gray-matter';
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((f) => f.startsWith('reference/') && f.endsWith('.md'));
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
- let data;
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((p) => p.data.api.file === oasFilename);
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((r) => r.fixable)) {
113
- const syncResults = syncOas(gitRoot);
114
- if (syncResults) {
115
- for (const r of results) {
116
- if (r.fixable) r.message += ' (fixed)';
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
 
@@ -1,6 +1,8 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+
3
4
  import OASNormalize from 'oas-normalize';
5
+
4
6
  import { findOasFiles } from '../commands/oas-sync.js';
5
7
 
6
8
  export const name = 'oas-schema';
@@ -33,7 +33,7 @@ function parseOrderYaml(content) {
33
33
  return content
34
34
  .split('\n')
35
35
  .map(parseOrderEntry)
36
- .filter((entry) => entry !== null);
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((slug) => !ordered.has(slug)).sort();
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((slug) => `- ${yamlSafeSlug(slug)}`).join('\n');
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((line) => {
183
+ .filter(line => {
184
184
  const entry = parseOrderEntry(line);
185
185
  return entry === null || !toRemove.has(entry);
186
186
  });
187
- if (kept.some((line) => parseOrderEntry(line) !== null)) {
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 matter from 'gray-matter';
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
- let data;
80
- let body;
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((b) => b.lang).join(', ');
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.