@writedocs/generator 0.4.8 → 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 (68) hide show
  1. package/astro.config.mjs +45 -2
  2. package/bin/writedocs.js +190 -139
  3. package/package.json +2 -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/AppIcon.astro +14 -2
  11. package/src/components/Badge.astro +2 -0
  12. package/src/components/Callout.astro +2 -1
  13. package/src/components/Card.astro +2 -1
  14. package/src/components/CardGroup.astro +2 -1
  15. package/src/components/Check.astro +1 -1
  16. package/src/components/CodeBlock.astro +94 -0
  17. package/src/components/CodeGroup.astro +2 -1
  18. package/src/components/Color.astro +2 -1
  19. package/src/components/ColorItem.astro +2 -1
  20. package/src/components/ColorRow.astro +2 -1
  21. package/src/components/Column.astro +19 -0
  22. package/src/components/Columns.astro +1 -1
  23. package/src/components/Danger.astro +1 -1
  24. package/src/components/Expandable.astro +2 -1
  25. package/src/components/Frame.astro +2 -1
  26. package/src/components/GitHubRepo.astro +2 -1
  27. package/src/components/Hint.astro +2 -1
  28. package/src/components/Icon.astro +3 -2
  29. package/src/components/Image.astro +2 -1
  30. package/src/components/Info.astro +1 -1
  31. package/src/components/Note.astro +1 -1
  32. package/src/components/Panel.astro +2 -1
  33. package/src/components/Parameter.astro +2 -1
  34. package/src/components/Prompt.astro +2 -1
  35. package/src/components/RequestExample.astro +2 -1
  36. package/src/components/ResponseExample.astro +2 -1
  37. package/src/components/Searchbar.astro +2 -1
  38. package/src/components/Step.astro +2 -1
  39. package/src/components/Steps.astro +4 -1
  40. package/src/components/Tab.astro +2 -1
  41. package/src/components/Tabs.astro +2 -1
  42. package/src/components/Tile.astro +2 -1
  43. package/src/components/Tip.astro +1 -1
  44. package/src/components/TreeFile.astro +2 -1
  45. package/src/components/TreeFolder.astro +2 -1
  46. package/src/components/Update.astro +2 -1
  47. package/src/components/Video.astro +2 -1
  48. package/src/components/View.astro +2 -1
  49. package/src/components/Warning.astro +1 -1
  50. package/src/components/class-names.ts +8 -0
  51. package/src/components/index.ts +2 -0
  52. package/src/content.config.ts +29 -129
  53. package/src/lib/config-schema.js +124 -0
  54. package/src/lib/config-schema.ts +1574 -1437
  55. package/src/lib/config.ts +7 -140
  56. package/src/lib/content-check.js +262 -0
  57. package/src/lib/icons.js +109 -0
  58. package/src/lib/mdx-auto-hydrate.js +12 -0
  59. package/src/lib/mdx-inject-builtins.js +16 -1
  60. package/src/lib/mdx-inline-react.js +202 -0
  61. package/src/lib/mdx-mintlify.js +65 -0
  62. package/src/lib/mdx-substitute-variables.js +17 -0
  63. package/src/lib/mdx-unknown-components.js +149 -0
  64. package/src/lib/mintlify-convert.js +599 -0
  65. package/src/lib/openapi-ref.js +44 -0
  66. package/src/lib/openapi-render.ts +10 -1
  67. package/src/lib/pages.js +150 -0
  68. package/src/pages/[...slug].astro +8 -2
@@ -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';
@@ -1,11 +1,11 @@
1
1
  import { defineCollection } from 'astro:content';
2
2
  import { glob } from 'astro/loaders';
3
3
  import type { Loader } from 'astro/loaders';
4
- import { z } from 'astro/zod';
5
4
  import fs from 'node:fs';
6
5
  import path from 'node:path';
7
6
  import { fileURLToPath, pathToFileURL } from 'node:url';
8
- import { seoFieldsSchema, findAllPages } from './lib/config';
7
+ import { pageFrontmatterSchema, findAllPages } from './lib/config';
8
+ import { titleFromPath } from './lib/pages.js';
9
9
  import { writedocsTempDir } from './lib/writedocs-temp-dir.js';
10
10
 
11
11
  const contentDir = process.env.WRITEDOCS_CONTENT_DIR || process.cwd();
@@ -32,131 +32,11 @@ const contentDir = process.env.WRITEDOCS_CONTENT_DIR || process.cwd();
32
32
  const generatedDocsDir = path.join(writedocsTempDir(contentDir), 'generated-docs');
33
33
  const generatedDocsBase = pathToFileURL(generatedDocsDir + path.sep);
34
34
 
35
- const MINTLIFY_MODE_ALIASES: Record<string, string> = { center: 'frame', assistant: 'default' };
36
-
37
- const docsSchema = z.object({
38
- title: z.string(),
39
- description: z.string().optional(),
40
- // Overrides the URL this page is served at, independent of where the
41
- // file actually lives - writedocs.json's `pages` arrays always keep
42
- // referencing the file's own path regardless. This is Astro's own
43
- // glob()-loader convention (a `slug` frontmatter field becomes
44
- // `entry.id` verbatim - see generateIdDefault in
45
- // astro/dist/content/loaders/glob.js), not writedocs-specific
46
- // behavior; declaring it here just brings it into the schema (and
47
- // its docs) rather than leaving it an undocumented Astro feature.
48
- // See fileIdForEntry() in lib/config.ts for how routes/links still
49
- // resolve a page by its file id once this diverges from `entry.id`.
50
- // Leading/trailing slashes are fine either way ("/", "/guides/x",
51
- // "guides/x" and "guides/x/" all mean the same thing) - see
52
- // normalizeEntryId() in lib/config.ts, which is what actually
53
- // strips them before this value is ever used as a route or href.
54
- slug: z.string().optional(),
55
- // Marks this page as an OpenAPI operation reference: "METHOD /path"
56
- // matching an operation in the owning group's OpenAPI spec, e.g.
57
- // "GET /pets/{petId}" - [...slug].astro renders <ApiPlayground /> for
58
- // any page with this field set, below the page's own MDX body (if
59
- // any). Sites don't write this by hand for most pages; it's either
60
- // set on a generated stub in the `generatedDocs` collection below (see
61
- // generate-api-pages.js) or hand-authored to "eject" one specific
62
- // operation into a real file (in `docs`) with custom prose - either
63
- // way, the value is always exactly the same "METHOD /path" key
64
- // generate-api-pages.js uses to look up the operation's full resolved
65
- // schema/examples at render time.
66
- openapi: z.string().optional(),
67
- // Controls how much of the site's own chrome (topbar, sidebar, table of
68
- // contents) wraps this page - see BaseLayout.astro/[...slug].astro for
69
- // what each value actually removes:
70
- // default - the normal three-column reading layout (all chrome).
71
- // wide - drops the table of contents; the article itself also
72
- // renders wider, for content that wants the extra room
73
- // (wide tables, side-by-side images).
74
- // frame - drops the sidebar and table of contents, but keeps the
75
- // topbar and the article's own normal presentation
76
- // ("frame" as in: still inside the site's outer frame).
77
- // custom - drops the sidebar and table of contents, the
78
- // auto-rendered <h1>, and prev/next nav, and skips the
79
- // article's own prose width/padding too - a blank canvas
80
- // for a hand-built landing/home page made entirely of
81
- // components - but keeps the topbar, so the page still
82
- // has site branding/nav/search/theme-toggle available.
83
- // blank - everything 'custom' drops, plus the topbar too: no site
84
- // chrome at all, just <slot />. For a page that wants to
85
- // look nothing like the rest of the site (an auth screen,
86
- // a print-style page).
87
- //
88
- // Two Mintlify values are accepted so a migrated page doesn't fail the
89
- // build (see MINTLIFY_MODE_ALIASES below): `center` is the same layout
90
- // as our `frame`, and `assistant` (a full-page Mintlify AI chat, which
91
- // writedocs doesn't have) falls back to `default`. Mintlify's own
92
- // `frame` means something else (a canvas that keeps the sidebar) - it's
93
- // left as our `frame`; see docs/dev/docs/mintlify-compat.mdx.
94
- mode: z
95
- .preprocess(
96
- (value) => (typeof value === 'string' && value in MINTLIFY_MODE_ALIASES ? MINTLIFY_MODE_ALIASES[value] : value),
97
- z.enum(['default', 'wide', 'frame', 'custom', 'blank']),
98
- )
99
- .default('default'),
100
- // Per-page meta tag overrides - same shape as writedocs.json's top-level
101
- // `seo` (see seoFieldsSchema in lib/config.ts, the single source of
102
- // truth for this shape). A page only needs to set the specific fields
103
- // it wants to override; mergeSeo() falls back to the site-wide default
104
- // for anything left unset. See BaseLayout.astro for where this and the
105
- // site-wide seo actually get merged and rendered.
106
- seo: seoFieldsSchema.optional(),
107
- // Mintlify's spelling of `seo.noindex` - a top-level `noindex: true`.
108
- // Folded into `seo` by the transform below, so everything downstream
109
- // (the robots meta tag, sitemap.xml, llms.txt) keeps reading
110
- // `seo.noindex` only.
111
- noindex: z.boolean().optional(),
112
- // Mintlify's short navigation label - used for the sidebar and the
113
- // topbar dropdown menus in place of `title` ([...slug].astro's
114
- // navTitleForSlug). The page's own <h1> and prev/next links keep `title`.
115
- sidebarTitle: z.string().optional(),
116
- // Mintlify's spellings of `seo.keywords`, `seo.ogImage`, `seo.ogType`
117
- // and `seo.twitterCard`, folded into `seo` below like `noindex`.
118
- keywords: z.array(z.string()).optional(),
119
- 'og:image': z.string().optional(),
120
- 'og:type': z.string().optional(),
121
- 'twitter:card': z.enum(['summary', 'summary_large_image']).optional(),
122
- // Mintlify's sidebar/page-chrome fields - see NavTree.astro and
123
- // [...slug].astro for where each is used:
124
- // icon - shown before the page's label in the sidebar.
125
- // tag - a short label after it (e.g. "NEW").
126
- // deprecated - a "Deprecated" label in the sidebar and next
127
- // to the page's <h1>.
128
- // hidden - left out of the sidebar, dropdowns and
129
- // prev/next, but still built and reachable by
130
- // URL. Also noindexed, as on Mintlify.
131
- // url - an external link: the page's sidebar entry
132
- // links straight to it, and the page's own URL
133
- // redirects there.
134
- // hideFooterPagination - no prev/next links on this page.
135
- // hideApiMarker - no HTTP method badge on this page's sidebar
136
- // entry.
137
- icon: z.string().optional(),
138
- tag: z.string().optional(),
139
- deprecated: z.boolean().optional(),
140
- hidden: z.boolean().optional(),
141
- url: z.string().optional(),
142
- hideFooterPagination: z.boolean().optional(),
143
- hideApiMarker: z.boolean().optional(),
144
- }).transform(
145
- ({ noindex, keywords, 'og:image': ogImage, 'og:type': ogType, 'twitter:card': twitterCard, ...data }) => {
146
- // Top-level Mintlify keys fill in `seo` only where the page's own `seo`
147
- // doesn't already set that field - an explicit `seo` value always wins.
148
- // A `hidden` page is noindexed unless it says otherwise.
149
- const fromMintlify = { noindex: noindex ?? (data.hidden ? true : undefined), keywords, ogImage, ogType, twitterCard };
150
- const seo = { ...data.seo };
151
- let changed = false;
152
- for (const [key, value] of Object.entries(fromMintlify)) {
153
- if (value === undefined || seo[key as keyof typeof seo] !== undefined) continue;
154
- (seo as Record<string, unknown>)[key] = value;
155
- changed = true;
156
- }
157
- return changed ? { ...data, seo } : data;
158
- },
159
- );
35
+ // Page frontmatter - defined in lib/config-schema.ts as
36
+ // pageFrontmatterSchema, next to writedocs.json's own schema, so
37
+ // `writedocs validate` checks every page against exactly the schema the
38
+ // build uses (that file compiles to the plain JS validate loads).
39
+ const docsSchema = pageFrontmatterSchema;
160
40
 
161
41
  /** Wraps another loader so it's skipped entirely - no filesystem scan, no
162
42
  * "directory doesn't exist"/"no files found" warning - when its own base
@@ -172,11 +52,31 @@ function skipIfMissing(inner: Loader): Loader {
172
52
  name: inner.name,
173
53
  load: async (context) => {
174
54
  if (!fs.existsSync(generatedDocsBase)) return;
175
- return inner.load(context);
55
+ return inner.load(withTitleFallback(context));
176
56
  },
177
57
  };
178
58
  }
179
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
+
180
80
  const generatedDocs = defineCollection({
181
81
  loader: skipIfMissing(glob({ pattern: '**/*.{md,mdx}', base: generatedDocsBase })),
182
82
  schema: docsSchema,
@@ -256,7 +156,7 @@ function livePages(dir: string, base: URL): Loader {
256
156
  for (const id of store.keys()) store.delete(id);
257
157
  return;
258
158
  }
259
- await glob({ pattern: files, base }).load({ ...context, watcher: undefined });
159
+ await glob({ pattern: files, base }).load({ ...withTitleFallback(context), watcher: undefined });
260
160
  }
261
161
 
262
162
  await resync();
@@ -259,6 +259,125 @@ function mergeSeo(site, page) {
259
259
  noindex: page.noindex ?? site.noindex
260
260
  };
261
261
  }
262
+ const MINTLIFY_MODE_ALIASES = { center: "frame", assistant: "default" };
263
+ const pageFrontmatterSchema = z.object({
264
+ title: z.string(),
265
+ description: z.string().optional(),
266
+ // Overrides the URL this page is served at, independent of where the
267
+ // file actually lives - writedocs.json's `pages` arrays always keep
268
+ // referencing the file's own path regardless. This is Astro's own
269
+ // glob()-loader convention (a `slug` frontmatter field becomes
270
+ // `entry.id` verbatim - see generateIdDefault in
271
+ // astro/dist/content/loaders/glob.js), not writedocs-specific
272
+ // behavior; declaring it here just brings it into the schema (and
273
+ // its docs) rather than leaving it an undocumented Astro feature.
274
+ // See fileIdForEntry() in lib/config.ts for how routes/links still
275
+ // resolve a page by its file id once this diverges from `entry.id`.
276
+ // Leading/trailing slashes are fine either way ("/", "/guides/x",
277
+ // "guides/x" and "guides/x/" all mean the same thing) - see
278
+ // normalizeEntryId() in lib/config.ts, which is what actually
279
+ // strips them before this value is ever used as a route or href.
280
+ slug: z.string().optional(),
281
+ // Marks this page as an OpenAPI operation reference: "METHOD /path"
282
+ // matching an operation in the owning group's OpenAPI spec, e.g.
283
+ // "GET /pets/{petId}" - [...slug].astro renders <ApiPlayground /> for
284
+ // any page with this field set, below the page's own MDX body (if
285
+ // any). Sites don't write this by hand for most pages; it's either
286
+ // set on a generated stub in the `generatedDocs` collection below (see
287
+ // generate-api-pages.js) or hand-authored to "eject" one specific
288
+ // operation into a real file (in `docs`) with custom prose - either
289
+ // way, the value is always exactly the same "METHOD /path" key
290
+ // generate-api-pages.js uses to look up the operation's full resolved
291
+ // schema/examples at render time.
292
+ openapi: z.string().optional(),
293
+ // Controls how much of the site's own chrome (topbar, sidebar, table of
294
+ // contents) wraps this page - see BaseLayout.astro/[...slug].astro for
295
+ // what each value actually removes:
296
+ // default - the normal three-column reading layout (all chrome).
297
+ // wide - drops the table of contents; the article itself also
298
+ // renders wider, for content that wants the extra room
299
+ // (wide tables, side-by-side images).
300
+ // frame - drops the sidebar and table of contents, but keeps the
301
+ // topbar and the article's own normal presentation
302
+ // ("frame" as in: still inside the site's outer frame).
303
+ // custom - drops the sidebar and table of contents, the
304
+ // auto-rendered <h1>, and prev/next nav, and skips the
305
+ // article's own prose width/padding too - a blank canvas
306
+ // for a hand-built landing/home page made entirely of
307
+ // components - but keeps the topbar, so the page still
308
+ // has site branding/nav/search/theme-toggle available.
309
+ // blank - everything 'custom' drops, plus the topbar too: no site
310
+ // chrome at all, just <slot />. For a page that wants to
311
+ // look nothing like the rest of the site (an auth screen,
312
+ // a print-style page).
313
+ //
314
+ // Two Mintlify values are accepted so a migrated page doesn't fail the
315
+ // build (see MINTLIFY_MODE_ALIASES above): `center` is the same layout
316
+ // as our `frame`, and `assistant` (a full-page Mintlify AI chat, which
317
+ // writedocs doesn't have) falls back to `default`. Mintlify's own
318
+ // `frame` means something else (a canvas that keeps the sidebar) - it's
319
+ // left as our `frame`; see docs/dev/docs/mintlify-compat.mdx.
320
+ mode: z.preprocess(
321
+ (value) => typeof value === "string" && value in MINTLIFY_MODE_ALIASES ? MINTLIFY_MODE_ALIASES[value] : value,
322
+ z.enum(["default", "wide", "frame", "custom", "blank"])
323
+ ).default("default"),
324
+ // Per-page meta tag overrides - same shape as writedocs.json's top-level
325
+ // `seo` (see seoFieldsSchema in lib/config.ts, the single source of
326
+ // truth for this shape). A page only needs to set the specific fields
327
+ // it wants to override; mergeSeo() falls back to the site-wide default
328
+ // for anything left unset. See BaseLayout.astro for where this and the
329
+ // site-wide seo actually get merged and rendered.
330
+ seo: seoFieldsSchema.optional(),
331
+ // Mintlify's spelling of `seo.noindex` - a top-level `noindex: true`.
332
+ // Folded into `seo` by the transform below, so everything downstream
333
+ // (the robots meta tag, sitemap.xml, llms.txt) keeps reading
334
+ // `seo.noindex` only.
335
+ noindex: z.boolean().optional(),
336
+ // Mintlify's short navigation label - used for the sidebar and the
337
+ // topbar dropdown menus in place of `title` ([...slug].astro's
338
+ // navTitleForSlug). The page's own <h1> and prev/next links keep `title`.
339
+ sidebarTitle: z.string().optional(),
340
+ // Mintlify's spellings of `seo.keywords`, `seo.ogImage`, `seo.ogType`
341
+ // and `seo.twitterCard`, folded into `seo` below like `noindex`.
342
+ keywords: z.array(z.string()).optional(),
343
+ "og:image": z.string().optional(),
344
+ "og:type": z.string().optional(),
345
+ "twitter:card": z.enum(["summary", "summary_large_image"]).optional(),
346
+ // Mintlify's sidebar/page-chrome fields - see NavTree.astro and
347
+ // [...slug].astro for where each is used:
348
+ // icon - shown before the page's label in the sidebar.
349
+ // tag - a short label after it (e.g. "NEW").
350
+ // deprecated - a "Deprecated" label in the sidebar and next
351
+ // to the page's <h1>.
352
+ // hidden - left out of the sidebar, dropdowns and
353
+ // prev/next, but still built and reachable by
354
+ // URL. Also noindexed, as on Mintlify.
355
+ // url - an external link: the page's sidebar entry
356
+ // links straight to it, and the page's own URL
357
+ // redirects there.
358
+ // hideFooterPagination - no prev/next links on this page.
359
+ // hideApiMarker - no HTTP method badge on this page's sidebar
360
+ // entry.
361
+ icon: z.string().optional(),
362
+ tag: z.string().optional(),
363
+ deprecated: z.boolean().optional(),
364
+ hidden: z.boolean().optional(),
365
+ url: z.string().optional(),
366
+ hideFooterPagination: z.boolean().optional(),
367
+ hideApiMarker: z.boolean().optional()
368
+ }).transform(
369
+ ({ noindex, keywords, "og:image": ogImage, "og:type": ogType, "twitter:card": twitterCard, ...data }) => {
370
+ const fromMintlify = { noindex: noindex ?? (data.hidden ? true : void 0), keywords, ogImage, ogType, twitterCard };
371
+ const seo = { ...data.seo };
372
+ let changed = false;
373
+ for (const [key, value] of Object.entries(fromMintlify)) {
374
+ if (value === void 0 || seo[key] !== void 0) continue;
375
+ seo[key] = value;
376
+ changed = true;
377
+ }
378
+ return changed ? { ...data, seo } : data;
379
+ }
380
+ );
262
381
  const apiSchema = z.object({
263
382
  // Whether the Try-it modal's Send button routes its request through
264
383
  // writedocs' own CORS proxy (https://proxy.writechoice.io/) rather
@@ -505,6 +624,9 @@ function desembrulharUnioes(issues, prefixo = []) {
505
624
  }
506
625
  return saida;
507
626
  }
627
+ function createJsonLocator(rawText) {
628
+ return criarLocalizador(rawText);
629
+ }
508
630
  function criarLocalizador(rawText) {
509
631
  let arvore;
510
632
  try {
@@ -700,10 +822,12 @@ function validateDocsConfig(rawText) {
700
822
  }
701
823
  export {
702
824
  ROOT_ALLOWED_EXTRA_KEYS,
825
+ createJsonLocator,
703
826
  docsConfigSchema,
704
827
  formatValidationIssues,
705
828
  formatValidationIssuesDetailed,
706
829
  mergeSeo,
830
+ pageFrontmatterSchema,
707
831
  seoFieldsSchema,
708
832
  unknownRootKeyIssues,
709
833
  validateDocsConfig