@writedocs/generator 0.4.9 → 0.4.10

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 (63) hide show
  1. package/astro.config.mjs +39 -2
  2. package/bin/writedocs.js +23 -0
  3. package/package.json +1 -1
  4. package/src/cli/convert.js +82 -0
  5. package/src/cli/generate-api-pages.js +56 -3
  6. package/src/components/Accordion.astro +2 -1
  7. package/src/components/AccordionGroup.astro +4 -1
  8. package/src/components/ApiPlayground.astro +6 -2
  9. package/src/components/ApiReferencePanel.astro +6 -2
  10. package/src/components/Badge.astro +2 -0
  11. package/src/components/Callout.astro +2 -1
  12. package/src/components/Card.astro +2 -1
  13. package/src/components/CardGroup.astro +2 -1
  14. package/src/components/Check.astro +1 -1
  15. package/src/components/CodeBlock.astro +94 -0
  16. package/src/components/CodeGroup.astro +2 -1
  17. package/src/components/Color.astro +2 -1
  18. package/src/components/ColorItem.astro +2 -1
  19. package/src/components/ColorRow.astro +2 -1
  20. package/src/components/Column.astro +19 -0
  21. package/src/components/Columns.astro +1 -1
  22. package/src/components/Danger.astro +1 -1
  23. package/src/components/Expandable.astro +2 -1
  24. package/src/components/Frame.astro +2 -1
  25. package/src/components/GitHubRepo.astro +2 -1
  26. package/src/components/Hint.astro +2 -1
  27. package/src/components/Icon.astro +3 -2
  28. package/src/components/Image.astro +2 -1
  29. package/src/components/Info.astro +1 -1
  30. package/src/components/Note.astro +1 -1
  31. package/src/components/Panel.astro +2 -1
  32. package/src/components/Parameter.astro +2 -1
  33. package/src/components/Prompt.astro +2 -1
  34. package/src/components/RequestExample.astro +2 -1
  35. package/src/components/ResponseExample.astro +2 -1
  36. package/src/components/Searchbar.astro +2 -1
  37. package/src/components/Step.astro +2 -1
  38. package/src/components/Steps.astro +4 -1
  39. package/src/components/Tab.astro +2 -1
  40. package/src/components/Tabs.astro +2 -1
  41. package/src/components/Tile.astro +2 -1
  42. package/src/components/Tip.astro +1 -1
  43. package/src/components/TreeFile.astro +2 -1
  44. package/src/components/TreeFolder.astro +2 -1
  45. package/src/components/Update.astro +2 -1
  46. package/src/components/Video.astro +2 -1
  47. package/src/components/View.astro +2 -1
  48. package/src/components/Warning.astro +1 -1
  49. package/src/components/class-names.ts +8 -0
  50. package/src/components/index.ts +2 -0
  51. package/src/content.config.ts +23 -2
  52. package/src/lib/content-check.js +36 -6
  53. package/src/lib/mdx-auto-hydrate.js +12 -0
  54. package/src/lib/mdx-inject-builtins.js +15 -0
  55. package/src/lib/mdx-inline-react.js +202 -0
  56. package/src/lib/mdx-mintlify.js +65 -0
  57. package/src/lib/mdx-substitute-variables.js +17 -0
  58. package/src/lib/mdx-unknown-components.js +56 -3
  59. package/src/lib/mintlify-convert.js +599 -0
  60. package/src/lib/openapi-ref.js +44 -0
  61. package/src/lib/openapi-render.ts +10 -1
  62. package/src/lib/pages.js +89 -17
  63. package/src/pages/[...slug].astro +8 -2
@@ -1,4 +1,5 @@
1
1
  ---
2
+ import { extraClasses } from './class-names';
2
3
  // Mintlify's <GitHub.Repo repo="owner/name" variant="inset|flat"> - a card
3
4
  // linking to a public GitHub repository. Attached as GitHub.Repo in
4
5
  // components/compound.ts. `repo` is an "owner/name" slug or a full
@@ -27,7 +28,7 @@ const valid = /^[\w.-]+\/[\w.-]+$/.test(slug);
27
28
  ---
28
29
  {valid && (
29
30
  <a
30
- class:list={['wd-github-repo', `wd-github-repo-${variant}`]}
31
+ class:list={['wd-github-repo', `wd-github-repo-${variant}`, extraClasses(Astro.props)]}
31
32
  href={`https://github.com/${slug}`}
32
33
  target="_blank"
33
34
  rel="noopener noreferrer"
@@ -1,4 +1,5 @@
1
1
  ---
2
+ import { extraClasses } from './class-names';
2
3
  // Inline, text-level component - unlike every other built-in here
3
4
  // (Callout, Card, Accordion, ...), which are all block-level and get
4
5
  // their own line, a Hint sits mid-sentence: `word <Hint tip="...">this
@@ -28,7 +29,7 @@ const { tip, headline, cta, href } = Astro.props as Props;
28
29
  const hasLink = Boolean(cta && href);
29
30
  ---
30
31
 
31
- <span class:list={["wd-hint", { "wd-hint-rich": Boolean(headline) || hasLink }]} tabindex="0">
32
+ <span class:list={["wd-hint", { "wd-hint-rich": Boolean(headline) || hasLink }, extraClasses(Astro.props)]} tabindex="0">
32
33
  <span class="wd-hint-trigger"><slot /></span>
33
34
  <span class="wd-hint-tooltip" role="tooltip">
34
35
  {headline && <span class="wd-hint-headline">{headline}</span>}
@@ -1,4 +1,5 @@
1
1
  ---
2
+ import { extraClasses } from './class-names';
2
3
  // Renders either a real icon (via `icon`) or a custom image (via
3
4
  // `src`) inline with surrounding text - modeled on Mintlify's own Icon
4
5
  // (https://www.mintlify.com/docs/components/icons), which documents
@@ -42,9 +43,9 @@ const iconStyle =
42
43
 
43
44
  {
44
45
  src ? (
45
- <img src={src} alt={alt} class="wd-icon-image" style={`height:${height}`} />
46
+ <img src={src} alt={alt} class:list={["wd-icon-image", extraClasses(Astro.props)]} style={`height:${height}`} />
46
47
  ) : icon ? (
47
- <AppIcon icon={icon} class="wd-icon-inline" style={iconStyle} />
48
+ <AppIcon icon={icon} class={["wd-icon-inline", ...extraClasses(Astro.props)].join(" ")} style={iconStyle} />
48
49
  ) : null
49
50
  }
50
51
  <style>
@@ -1,4 +1,5 @@
1
1
  ---
2
+ import { extraClasses } from './class-names';
2
3
  // Block-level (own line, centered) - unlike Hint (inline/text-level),
3
4
  // this is meant to be dropped in on its own the way Callout/Card are.
4
5
  //
@@ -65,7 +66,7 @@ const sizeStyle = size ? `--wd-image-size:${size}` : undefined;
65
66
  ---
66
67
 
67
68
  <figure
68
- class:list={["wd-image-figure", caption && "wd-image-figure-framed", srcDark && "wd-image-has-dark"]}
69
+ class:list={["wd-image-figure", caption && "wd-image-figure-framed", srcDark && "wd-image-has-dark", extraClasses(Astro.props)]}
69
70
  style={sizeStyle}
70
71
  >
71
72
  <img src={src} alt={alt} class="wd-image wd-image-light" nozoom={noZoom || undefined} />
@@ -9,4 +9,4 @@ interface Props {
9
9
  }
10
10
  const { title, _titleId } = Astro.props as Props;
11
11
  ---
12
- <Callout type="info" title={title} _titleId={_titleId}><slot /></Callout>
12
+ <Callout type="info" title={title} _titleId={_titleId} class={Astro.props.class} className={Astro.props.className}><slot /></Callout>
@@ -9,4 +9,4 @@ interface Props {
9
9
  }
10
10
  const { title, _titleId } = Astro.props as Props;
11
11
  ---
12
- <Callout type="note" title={title} _titleId={_titleId}><slot /></Callout>
12
+ <Callout type="note" title={title} _titleId={_titleId} class={Astro.props.class} className={Astro.props.className}><slot /></Callout>
@@ -1,4 +1,5 @@
1
1
  ---
2
+ import { extraClasses } from './class-names';
2
3
  // Mintlify's <Panel> - content pinned to the right-hand column on desktop,
3
4
  // in place of the table of contents. It's the same docking
4
5
  // RequestExample/ResponseExample already use (initExamplePanels() in
@@ -8,4 +9,4 @@
8
9
  // with no table-of-contents column (any mode but `default`), the Panel
9
10
  // stays where it is in the page.
10
11
  ---
11
- <div class="wd-example-panel wd-panel"><slot /></div>
12
+ <div class:list={["wd-example-panel wd-panel", extraClasses(Astro.props)]}><slot /></div>
@@ -1,4 +1,5 @@
1
1
  ---
2
+ import { extraClasses } from './class-names';
2
3
  // The name+type+description row used throughout API reference content -
3
4
  // modeled directly on Mintlify's own ParamField/ResponseField
4
5
  // (https://www.mintlify.com/docs/components/expandables), which this
@@ -38,7 +39,7 @@ const { name, type, required = false, default: defaultValue, deprecated = false,
38
39
  const hasDefault = defaultValue !== undefined && defaultValue !== null && defaultValue !== "";
39
40
  ---
40
41
 
41
- <div class:list={["wd-parameter", { "wd-parameter-deprecated": deprecated }]}>
42
+ <div class:list={["wd-parameter", { "wd-parameter-deprecated": deprecated }, extraClasses(Astro.props)]}>
42
43
  <div class="wd-parameter-head">
43
44
  {pre.map((label) => <span class="wd-parameter-label">{label}</span>)}
44
45
  <span class="wd-parameter-name">{name}</span>
@@ -1,4 +1,5 @@
1
1
  ---
2
+ import { extraClasses } from './class-names';
2
3
  // Mintlify's <Prompt> - a ready-made AI prompt: a card showing
3
4
  // `description` (inline Markdown) and an optional icon, with actions to
4
5
  // copy the prompt or open it in Cursor. The prompt itself is the slot; it's
@@ -27,7 +28,7 @@ const { description, icon, actions = ['copy'], _text } = Astro.props as Props;
27
28
  const cursorHref = _text ? `https://cursor.com/link/prompt?text=${encodeURIComponent(_text)}` : null;
28
29
  const showCursor = actions.includes('cursor') && cursorHref !== null && cursorHref.length <= 10000;
29
30
  ---
30
- <div class="wd-prompt">
31
+ <div class:list={["wd-prompt", extraClasses(Astro.props)]}>
31
32
  <div class="wd-prompt-head">
32
33
  {icon && <AppIcon icon={icon} class="wd-prompt-icon" />}
33
34
  {description && <div class="wd-prompt-description" set:html={inlineMarkdown(description)} />}
@@ -1,4 +1,5 @@
1
1
  ---
2
+ import { extraClasses } from './class-names';
2
3
  // Pins its code example(s) in the right sidebar - in place of the page's
3
4
  // normal table of contents - on desktop; falls back to a plain in-place
4
5
  // CodeGroup on narrower viewports where that sidebar column doesn't
@@ -26,7 +27,7 @@ interface Props {
26
27
  }
27
28
  const { dropdown = false } = Astro.props as Props;
28
29
  ---
29
- <div class="wd-example-panel wd-request-example">
30
+ <div class:list={["wd-example-panel wd-request-example", extraClasses(Astro.props)]}>
30
31
  <CodeGroup dropdown={dropdown}>
31
32
  <slot />
32
33
  </CodeGroup>
@@ -1,4 +1,5 @@
1
1
  ---
2
+ import { extraClasses } from './class-names';
2
3
  // See RequestExample.astro's own comment - identical in every respect
3
4
  // except the marker class (wd-response-example vs wd-request-example),
4
5
  // which src/scripts/example-panels.ts uses to always dock Response
@@ -12,7 +13,7 @@ interface Props {
12
13
  }
13
14
  const { dropdown = false } = Astro.props as Props;
14
15
  ---
15
- <div class="wd-example-panel wd-response-example">
16
+ <div class:list={["wd-example-panel wd-response-example", extraClasses(Astro.props)]}>
16
17
  <CodeGroup dropdown={dropdown}>
17
18
  <slot />
18
19
  </CodeGroup>
@@ -1,4 +1,5 @@
1
1
  ---
2
+ import { extraClasses } from './class-names';
2
3
  // Wraps a GFM table (or any other block-level slot content, though a
3
4
  // table is the intended/primary use case) with a text input above it
4
5
  // that live-filters rows by their own text content - "let users filter
@@ -21,7 +22,7 @@ interface Props {
21
22
  const { placeholder = "Search..." } = Astro.props as Props;
22
23
  ---
23
24
 
24
- <div class="wd-searchbar">
25
+ <div class:list={["wd-searchbar", extraClasses(Astro.props)]}>
25
26
  <input type="text" class="wd-searchbar-input" placeholder={placeholder} aria-label={placeholder} />
26
27
  <div class="wd-searchbar-content">
27
28
  <slot />
@@ -1,4 +1,5 @@
1
1
  ---
2
+ import { extraClasses } from './class-names';
2
3
  import AppIcon from './AppIcon.astro';
3
4
  // `icon`, `stepNumber` and `titleSize` are Mintlify's. `icon` replaces the
4
5
  // number in the step's circle. `stepNumber` sets this step's number; the
@@ -15,7 +16,7 @@ const { title, icon, stepNumber, titleSize = 'p' } = Astro.props as Props;
15
16
  const style = typeof stepNumber === 'number' ? `counter-set: wd-step ${stepNumber - 1}` : undefined;
16
17
  const TitleTag = titleSize === 'h2' || titleSize === 'h3' ? titleSize : 'p';
17
18
  ---
18
- <div class:list={['wd-step', { 'wd-step-has-icon': Boolean(icon) }]} style={style}>
19
+ <div class:list={['wd-step', { 'wd-step-has-icon': Boolean(icon) }, extraClasses(Astro.props)]} style={style}>
19
20
  {icon && <AppIcon icon={icon} class="wd-step-icon" />}
20
21
  {
21
22
  title &&
@@ -1,4 +1,7 @@
1
- <div class="wd-steps"><slot /></div>
1
+ ---
2
+ import { extraClasses } from './class-names';
3
+ ---
4
+ <div class:list={["wd-steps", extraClasses(Astro.props)]}><slot /></div>
2
5
  <style is:global>
3
6
  .wd-steps {
4
7
  counter-reset: wd-step;
@@ -1,4 +1,5 @@
1
1
  ---
2
+ import { extraClasses } from './class-names';
2
3
  import AppIcon from './AppIcon.astro';
3
4
  // `icon` is Mintlify's: shown before the title in the tab's button. The
4
5
  // button is built client-side by Tabs.astro, which moves this hidden,
@@ -9,7 +10,7 @@ interface Props {
9
10
  }
10
11
  const { title, icon } = Astro.props as Props;
11
12
  ---
12
- <div class="wd-tab" data-title={title}>
13
+ <div class:list={["wd-tab", extraClasses(Astro.props)]} data-title={title}>
13
14
  {icon && <span class="wd-tab-icon-src" hidden><AppIcon icon={icon} class="wd-tabs-btn-icon" /></span>}
14
15
  <slot />
15
16
  </div>
@@ -1,11 +1,12 @@
1
1
  ---
2
+ import { extraClasses } from './class-names';
2
3
  // `defaultTabIndex` is Mintlify's: which tab (0-based) starts selected.
3
4
  interface Props {
4
5
  defaultTabIndex?: number;
5
6
  }
6
7
  const { defaultTabIndex = 0 } = Astro.props as Props;
7
8
  ---
8
- <div class="wd-tabs" data-default-index={defaultTabIndex}>
9
+ <div class:list={["wd-tabs", extraClasses(Astro.props)]} data-default-index={defaultTabIndex}>
9
10
  <slot />
10
11
  </div>
11
12
  <script>
@@ -1,4 +1,5 @@
1
1
  ---
2
+ import { extraClasses } from './class-names';
2
3
  // Mintlify's <Tile> - a visual preview (the slot, usually an image or a
3
4
  // light/dark image pair) on a patterned background, with a title and
4
5
  // description under it. Usually laid out in a <Columns> grid. `title`
@@ -13,7 +14,7 @@ interface Props {
13
14
  const { href, title, description } = Astro.props as Props;
14
15
  const Tag = href ? 'a' : 'div';
15
16
  ---
16
- <Tag class="wd-tile" href={href}>
17
+ <Tag class:list={["wd-tile", extraClasses(Astro.props)]} href={href}>
17
18
  <div class="wd-tile-preview"><slot /></div>
18
19
  {title && <div class="wd-tile-title" set:html={inlineMarkdown(title)} />}
19
20
  {description && <div class="wd-tile-description">{description}</div>}
@@ -9,4 +9,4 @@ interface Props {
9
9
  }
10
10
  const { title, _titleId } = Astro.props as Props;
11
11
  ---
12
- <Callout type="tip" title={title} _titleId={_titleId}><slot /></Callout>
12
+ <Callout type="tip" title={title} _titleId={_titleId} class={Astro.props.class} className={Astro.props.className}><slot /></Callout>
@@ -1,4 +1,5 @@
1
1
  ---
2
+ import { extraClasses } from './class-names';
2
3
  // Mintlify's <Tree.File name="..."> - see Tree.astro. `highlight` marks it
3
4
  // as the active item.
4
5
  import AppIcon from './AppIcon.astro';
@@ -8,7 +9,7 @@ interface Props {
8
9
  }
9
10
  const { name, highlight = false } = Astro.props as Props;
10
11
  ---
11
- <li class:list={['wd-tree-item', 'wd-tree-file', { 'wd-tree-highlight': highlight }]} role="treeitem" tabindex="-1">
12
+ <li class:list={['wd-tree-item', 'wd-tree-file', { 'wd-tree-highlight': highlight }, extraClasses(Astro.props)]} role="treeitem" tabindex="-1">
12
13
  <span class="wd-tree-row">
13
14
  <span class="wd-tree-chevron wd-tree-chevron-spacer" aria-hidden="true"></span>
14
15
  <AppIcon icon="lucide:file" class="wd-tree-icon" />
@@ -1,4 +1,5 @@
1
1
  ---
2
+ import { extraClasses } from './class-names';
2
3
  // Mintlify's <Tree.Folder name="..."> - see Tree.astro. `defaultOpen`
3
4
  // starts it expanded; `openable={false}` makes it a fixed, non-toggling
4
5
  // folder (shown open or closed per `defaultOpen`); `highlight` marks it as
@@ -13,7 +14,7 @@ interface Props {
13
14
  const { name, defaultOpen = false, openable = true, highlight = false } = Astro.props as Props;
14
15
  ---
15
16
  <li
16
- class:list={['wd-tree-item', 'wd-tree-folder', { 'wd-tree-highlight': highlight, 'wd-tree-static': !openable }]}
17
+ class:list={['wd-tree-item', 'wd-tree-folder', { 'wd-tree-highlight': highlight, 'wd-tree-static': !openable }, extraClasses(Astro.props)]}
17
18
  role="treeitem"
18
19
  aria-expanded={defaultOpen ? 'true' : 'false'}
19
20
  tabindex="-1"
@@ -1,4 +1,5 @@
1
1
  ---
2
+ import { extraClasses } from './class-names';
2
3
  // Mintlify's <Update> - one changelog entry: `label` (usually a date or
3
4
  // version) in a left column that sticks while its content scrolls, with
4
5
  // `description` and `tags` under it. The label is a linkable anchor, deduped
@@ -25,7 +26,7 @@ function slugify(value: string): string {
25
26
  }
26
27
  const id = _titleId ?? slugify(label);
27
28
  ---
28
- <section class="wd-update" data-update-tags={JSON.stringify(tags)}>
29
+ <section class:list={["wd-update", extraClasses(Astro.props)]} data-update-tags={JSON.stringify(tags)}>
29
30
  <div class="wd-update-meta">
30
31
  <div class="wd-update-label" id={id}>
31
32
  <a href={`#${id}`}>{label}</a>
@@ -1,4 +1,5 @@
1
1
  ---
2
+ import { extraClasses } from './class-names';
2
3
  // Renders either a native <video> or an <iframe> embed from the same
3
4
  // `src` prop, auto-detected rather than requiring the author to pick -
4
5
  // `src="media/clip.mp4"` (a direct video file, local or remote) gets a
@@ -57,7 +58,7 @@ const sizeStyle = width ? `--wd-video-size:${width}` : undefined;
57
58
  const effectiveMuted = muted || autoplay;
58
59
  ---
59
60
 
60
- <figure class:list={["wd-video-figure", caption && "wd-video-figure-framed"]} style={sizeStyle}>
61
+ <figure class:list={["wd-video-figure", caption && "wd-video-figure-framed", extraClasses(Astro.props)]} style={sizeStyle}>
61
62
  {
62
63
  isFile ? (
63
64
  <video
@@ -1,4 +1,5 @@
1
1
  ---
2
+ import { extraClasses } from './class-names';
2
3
  // Mintlify's <View title="..." icon="..."> - content for one of several
3
4
  // alternatives (a language, a framework) on the same page. Every distinct
4
5
  // `title` on the page becomes an option in one switcher, placed under the
@@ -16,7 +17,7 @@ interface Props {
16
17
  }
17
18
  const { title, icon } = Astro.props as Props;
18
19
  ---
19
- <div class="wd-view" data-view-title={title}>
20
+ <div class:list={["wd-view", extraClasses(Astro.props)]} data-view-title={title}>
20
21
  {icon && <span class="wd-view-icon-src" hidden><AppIcon icon={icon} class="wd-view-icon" /></span>}
21
22
  <slot />
22
23
  </div>
@@ -9,4 +9,4 @@ interface Props {
9
9
  }
10
10
  const { title, _titleId } = Astro.props as Props;
11
11
  ---
12
- <Callout type="warning" title={title} _titleId={_titleId}><slot /></Callout>
12
+ <Callout type="warning" title={title} _titleId={_titleId} class={Astro.props.class} className={Astro.props.className}><slot /></Callout>
@@ -0,0 +1,8 @@
1
+ // Every built-in component takes extra classes for its root element - as
2
+ // `className` (how MDX, and Mintlify content, passes it) or `class` (Astro's
3
+ // own spelling). Components add extraClasses(Astro.props) to their root
4
+ // element's class:list; with neither set it adds nothing, so the rendered
5
+ // markup is unchanged.
6
+ export function extraClasses(props: Record<string, unknown>): string[] {
7
+ return [props.class, props.className].filter((c): c is string => typeof c === 'string' && c.trim() !== '');
8
+ }
@@ -50,6 +50,8 @@ export { default as Check } from './Check.astro';
50
50
  export { default as ParamField } from './ParamField.astro';
51
51
  export { default as ResponseField } from './ResponseField.astro';
52
52
  export { default as Columns } from './Columns.astro';
53
+ export { default as Column } from './Column.astro';
54
+ export { default as CodeBlock } from './CodeBlock.astro';
53
55
  export { default as Tooltip } from './Tooltip.astro';
54
56
  export { default as Update } from './Update.astro';
55
57
  export { default as Tile } from './Tile.astro';
@@ -5,6 +5,7 @@ import fs from 'node:fs';
5
5
  import path from 'node:path';
6
6
  import { fileURLToPath, pathToFileURL } from 'node:url';
7
7
  import { pageFrontmatterSchema, findAllPages } from './lib/config';
8
+ import { titleFromPath } from './lib/pages.js';
8
9
  import { writedocsTempDir } from './lib/writedocs-temp-dir.js';
9
10
 
10
11
  const contentDir = process.env.WRITEDOCS_CONTENT_DIR || process.cwd();
@@ -51,11 +52,31 @@ function skipIfMissing(inner: Loader): Loader {
51
52
  name: inner.name,
52
53
  load: async (context) => {
53
54
  if (!fs.existsSync(generatedDocsBase)) return;
54
- return inner.load(context);
55
+ return inner.load(withTitleFallback(context));
55
56
  },
56
57
  };
57
58
  }
58
59
 
60
+ // A page with no `title` in its frontmatter gets one from its file name,
61
+ // before the schema (which requires `title`) sees it - Mintlify's rule, so
62
+ // a migrated page that relied on it builds with the same title. See
63
+ // titleFromPath() in lib/pages.js; `writedocs validate` applies the same
64
+ // default (lib/content-check.js).
65
+ type LoaderContext = Parameters<Loader['load']>[0];
66
+ function withTitleFallback(context: LoaderContext): LoaderContext {
67
+ return {
68
+ ...context,
69
+ parseData: (props) =>
70
+ context.parseData({
71
+ ...props,
72
+ data:
73
+ typeof props.data?.title === 'string'
74
+ ? props.data
75
+ : { ...props.data, title: titleFromPath(props.filePath ?? props.id) },
76
+ }),
77
+ };
78
+ }
79
+
59
80
  const generatedDocs = defineCollection({
60
81
  loader: skipIfMissing(glob({ pattern: '**/*.{md,mdx}', base: generatedDocsBase })),
61
82
  schema: docsSchema,
@@ -135,7 +156,7 @@ function livePages(dir: string, base: URL): Loader {
135
156
  for (const id of store.keys()) store.delete(id);
136
157
  return;
137
158
  }
138
- await glob({ pattern: files, base }).load({ ...context, watcher: undefined });
159
+ await glob({ pattern: files, base }).load({ ...withTitleFallback(context), watcher: undefined });
139
160
  }
140
161
 
141
162
  await resync();
@@ -8,19 +8,23 @@
8
8
  // pageFrontmatterSchema the build uses) or isn't valid YAML
9
9
  // - an .mdx file doesn't parse as MDX
10
10
  // - writedocs.json's navigation lists a page that doesn't exist
11
+ // - a redirect is a pattern (`/old/:slug`) rather than one exact path
11
12
  // Warnings - the build succeeds, but not as written:
12
13
  // - an unknown component (the build shows only its content - see
13
14
  // lib/mdx-unknown-components.js)
14
15
  // - an icon name no installed icon set has (the build leaves it out -
15
16
  // see AppIcon.astro), in a component's `icon`, a page's `icon`, or
16
17
  // writedocs.json
18
+ // - a built-in component inside a page component that runs as React,
19
+ // where it renders simplified (lib/mdx-inline-react.js)
17
20
  import fs from 'node:fs';
18
21
  import path from 'node:path';
19
22
  import matter from 'gray-matter';
20
23
  import { visit } from 'unist-util-visit';
21
- import { findAllPages, fileIdForPath } from './pages.js';
24
+ import { findAllPages, fileIdForPath, titleFromPath } from './pages.js';
22
25
  import { iconExists } from './icons.js';
23
26
  import { findUnknownComponents } from './mdx-unknown-components.js';
27
+ import { findInteractiveComponents, simplifiedBuiltinMessage } from './mdx-inline-react.js';
24
28
  import { pageFrontmatterSchema, createJsonLocator } from './config-schema.js';
25
29
 
26
30
  /** An issue: { file, line?, message, suggestion? } - `file` relative to the
@@ -75,8 +79,10 @@ async function checkPage(contentDir, rel, errors, warnings) {
75
79
  // returns from its cache (findAllPages() already parsed every page once).
76
80
  const frontmatterText = raw.match(/^---[^\n]*\n([\s\S]*?)\n---/)?.[1] ?? '';
77
81
 
78
- // Frontmatter against the same schema the build uses.
79
- const result = pageFrontmatterSchema.safeParse(parsed.data);
82
+ // Frontmatter against the same schema the build uses - with the same
83
+ // file-name default for a missing title (content.config.ts).
84
+ const data = typeof parsed.data?.title === 'string' ? parsed.data : { ...parsed.data, title: titleFromPath(rel) };
85
+ const result = pageFrontmatterSchema.safeParse(data);
80
86
  if (!result.success) {
81
87
  for (const i of result.error.issues) {
82
88
  const key = String(i.path[0] ?? '');
@@ -122,6 +128,14 @@ async function checkPage(contentDir, rel, errors, warnings) {
122
128
  )
123
129
  );
124
130
  }
131
+ // Built-ins inside a page component that runs as React - they render
132
+ // simplified there (lib/mdx-inline-react.js, which the build warns from
133
+ // with the same words).
134
+ for (const use of findInteractiveComponents(tree, parsed.content).simplified) {
135
+ const line = parsed.content.slice(0, use.offset).split('\n').length + lineOffset;
136
+ const { message, suggestion } = simplifiedBuiltinMessage(use);
137
+ warnings.push(issue(rel, line, message, suggestion));
138
+ }
125
139
  visit(tree, ['mdxJsxFlowElement', 'mdxJsxTextElement'], (node) => {
126
140
  const attr = node.attributes?.find((a) => a.type === 'mdxJsxAttribute' && a.name === 'icon');
127
141
  if (!attr || typeof attr.value !== 'string' || iconExists(attr.value)) return;
@@ -195,18 +209,34 @@ export async function checkContent(contentDir, configText) {
195
209
  const ids = new Set(pages.map(fileIdForPath));
196
210
  for (const ref of navigationReferences(config.navigation)) {
197
211
  if (ids.has(ref.id)) continue;
198
- const withoutFrontmatter = ['.mdx', '.md'].some((ext) => fs.existsSync(path.join(contentDir, `${ref.id}${ext}`)));
212
+ const folderIndex = ref.id.endsWith('/index') && ids.has(ref.id.replace(/\/index$/, ''));
199
213
  errors.push(
200
214
  issue(
201
215
  'writedocs.json',
202
216
  locate(ref.path)?.line,
203
217
  `The navigation lists page "${ref.id}", but there's no page with that path.`,
204
- withoutFrontmatter
205
- ? `${ref.id} exists but has no frontmatter, so it isn't a page - add a frontmatter block with at least a title.`
218
+ folderIndex
219
+ ? `A folder's index page is listed by the folder's path - write "${ref.id.replace(/\/index$/, '')}".`
206
220
  : `Create ${ref.id}.mdx, or fix the path (it's relative to the folder writedocs.json is in, without the extension).`
207
221
  )
208
222
  );
209
223
  }
224
+ // A redirect is one exact path - a Mintlify-style pattern (`/old/:slug`,
225
+ // `/old/*`) becomes a literal page path, and the build fails on it.
226
+ (Array.isArray(config.redirects) ? config.redirects : []).forEach((r, i) => {
227
+ for (const key of ['source', 'destination']) {
228
+ const value = r?.[key];
229
+ if (typeof value !== 'string' || !/(^|\/):[A-Za-z_]|\*/.test(value)) continue;
230
+ errors.push(
231
+ issue(
232
+ 'writedocs.json',
233
+ locate(['redirects', i, key])?.line,
234
+ `Redirect ${key} "${value}" is a pattern - writedocs redirects match one exact path.`,
235
+ 'Replace it with one redirect per exact path.'
236
+ )
237
+ );
238
+ }
239
+ });
210
240
  for (const { icon, path: jsonPath } of configIcons(config)) {
211
241
  if (iconExists(icon)) continue;
212
242
  const { message, suggestion } = unknownIconMessage(icon);
@@ -63,6 +63,18 @@ export function remarkAutoHydrateSnippets() {
63
63
  (attr) => attr.type === 'mdxJsxAttribute' && typeof attr.name === 'string' && attr.name.startsWith(CLIENT_ATTR_PREFIX)
64
64
  );
65
65
  if (hasClientDirective) return;
66
+ // A component handed over as a prop (`Renderer={MyBlock}`, a pattern
67
+ // Mintlify content uses) can't be hydrated: Astro sends an island's
68
+ // props to the browser as data, and a function doesn't survive that -
69
+ // it arrives as null and React throws. Such a usage is rendered on the
70
+ // server only; everything else about it still works.
71
+ const passesComponent = attributes.some(
72
+ (attr) =>
73
+ attr.type === 'mdxJsxAttribute' &&
74
+ attr.value?.type === 'mdxJsxAttributeValueExpression' &&
75
+ /^\s*[A-Z][A-Za-z0-9]*\s*$/.test(attr.value.value ?? '')
76
+ );
77
+ if (passesComponent) return;
66
78
  attributes.push({ type: 'mdxJsxAttribute', name: 'client:load', value: null });
67
79
  node.attributes = attributes;
68
80
  });
@@ -38,6 +38,8 @@ export const BUILTIN_COMPONENT_NAMES = [
38
38
  'ParamField',
39
39
  'ResponseField',
40
40
  'Columns',
41
+ 'Column',
42
+ 'CodeBlock',
41
43
  'Tooltip',
42
44
  'Update',
43
45
  'Tile',
@@ -95,6 +97,19 @@ export function remarkInjectBuiltinComponents() {
95
97
  }
96
98
  });
97
99
 
100
+ // Built-ins used inside the page's own code, too - JSX in an
101
+ // `export const Widget = () => <Icon icon="x" />` or a `{...}`
102
+ // expression isn't part of the content tree above, and the components
103
+ // map [...slug].astro passes only reaches content, so without an import
104
+ // `Icon` is simply undefined there. Mintlify makes its components
105
+ // available in both places.
106
+ visit(tree, ['mdxjsEsm', 'mdxFlowExpression', 'mdxTextExpression'], (node) => {
107
+ for (const m of String(node.value ?? '').matchAll(/<([A-Z][A-Za-z0-9]*)[\s./>]/g)) {
108
+ const name = m[1];
109
+ if (BUILTIN_COMPONENT_NAMES.includes(name) && !locallyBound.has(name)) needed.add(name);
110
+ }
111
+ });
112
+
98
113
  if (needed.size === 0) return;
99
114
 
100
115
  const names = [...needed].sort();