@writedocs/generator 0.4.7 → 0.4.9
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/astro.config.mjs +13 -0
- package/bin/writedocs.js +167 -139
- package/package.json +2 -1
- package/src/components/AppIcon.astro +14 -2
- package/src/components/Color.astro +61 -0
- package/src/components/ColorItem.astro +93 -0
- package/src/components/ColorRow.astro +39 -0
- package/src/components/GitHubRepo.astro +156 -0
- package/src/components/Panel.astro +11 -0
- package/src/components/Prompt.astro +143 -0
- package/src/components/Tile.astro +65 -0
- package/src/components/Tree.astro +213 -0
- package/src/components/TreeFile.astro +17 -0
- package/src/components/TreeFolder.astro +28 -0
- package/src/components/Update.astro +171 -0
- package/src/components/View.astro +214 -0
- package/src/components/Visibility.astro +11 -0
- package/src/components/compound.ts +21 -0
- package/src/components/index.ts +7 -0
- package/src/content.config.ts +6 -127
- package/src/lib/config-schema.js +124 -0
- package/src/lib/config-schema.ts +1574 -1437
- package/src/lib/config.ts +7 -140
- package/src/lib/content-check.js +232 -0
- package/src/lib/icons.js +109 -0
- package/src/lib/inline-markdown.js +29 -0
- package/src/lib/mdx-inject-builtins.js +16 -3
- package/src/lib/mdx-mintlify.js +99 -0
- package/src/lib/mdx-title-anchor-ids.js +7 -2
- package/src/lib/mdx-unknown-components.js +96 -0
- package/src/lib/pages.js +78 -0
- package/src/lib/visibility.js +29 -0
- package/src/pages/[...slug].astro +23 -1
- package/src/pages/[...slug].md.ts +4 -1
- package/src/pages/llms-full.txt.ts +3 -1
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Mintlify's <Update> - one changelog entry: `label` (usually a date or
|
|
3
|
+
// version) in a left column that sticks while its content scrolls, with
|
|
4
|
+
// `description` and `tags` under it. The label is a linkable anchor, deduped
|
|
5
|
+
// against every other anchor on the page by remarkTitleAnchorIds, same as a
|
|
6
|
+
// Callout/Accordion title. Stacks into one column on narrow screens.
|
|
7
|
+
//
|
|
8
|
+
// When any Update on the page has `tags`, a row of tag filters appears
|
|
9
|
+
// above the first one (the script below) - Mintlify shows the same filters
|
|
10
|
+
// in its right-hand panel. `rss` is accepted but unused: writedocs doesn't
|
|
11
|
+
// generate an RSS feed from updates.
|
|
12
|
+
interface Props {
|
|
13
|
+
label: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
tags?: string[];
|
|
16
|
+
rss?: { title?: string; description?: string };
|
|
17
|
+
_titleId?: string;
|
|
18
|
+
}
|
|
19
|
+
const { label, description, tags = [], _titleId } = Astro.props as Props;
|
|
20
|
+
function slugify(value: string): string {
|
|
21
|
+
return value
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
24
|
+
.replace(/^-+|-+$/g, '');
|
|
25
|
+
}
|
|
26
|
+
const id = _titleId ?? slugify(label);
|
|
27
|
+
---
|
|
28
|
+
<section class="wd-update" data-update-tags={JSON.stringify(tags)}>
|
|
29
|
+
<div class="wd-update-meta">
|
|
30
|
+
<div class="wd-update-label" id={id}>
|
|
31
|
+
<a href={`#${id}`}>{label}</a>
|
|
32
|
+
</div>
|
|
33
|
+
{description && <div class="wd-update-description">{description}</div>}
|
|
34
|
+
{tags.length > 0 && (
|
|
35
|
+
<div class="wd-update-tags">
|
|
36
|
+
{tags.map((tag) => <span class="wd-update-tag">{tag}</span>)}
|
|
37
|
+
</div>
|
|
38
|
+
)}
|
|
39
|
+
</div>
|
|
40
|
+
<div class="wd-update-body"><slot /></div>
|
|
41
|
+
</section>
|
|
42
|
+
<script>
|
|
43
|
+
// Builds one tag-filter bar per page, before the first Update, from every
|
|
44
|
+
// Update's tags. Clicking a tag shows only the updates that have it;
|
|
45
|
+
// clicking it again (or "All") shows everything.
|
|
46
|
+
function initUpdateFilters(root: ParentNode) {
|
|
47
|
+
const updates = Array.from(root.querySelectorAll<HTMLElement>('.wd-update'));
|
|
48
|
+
if (updates.length === 0 || updates[0].dataset.wdInit) return;
|
|
49
|
+
updates.forEach((u) => (u.dataset.wdInit = 'true'));
|
|
50
|
+
const tagsOf = (u: HTMLElement): string[] => {
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(u.dataset.updateTags ?? '[]');
|
|
53
|
+
} catch {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const allTags = [...new Set(updates.flatMap(tagsOf))];
|
|
58
|
+
if (allTags.length === 0) return;
|
|
59
|
+
|
|
60
|
+
const bar = document.createElement('div');
|
|
61
|
+
bar.className = 'wd-update-filters';
|
|
62
|
+
bar.setAttribute('role', 'toolbar');
|
|
63
|
+
bar.setAttribute('aria-label', 'Filter updates by tag');
|
|
64
|
+
let active: string | null = null;
|
|
65
|
+
const buttons: HTMLButtonElement[] = [];
|
|
66
|
+
const apply = () => {
|
|
67
|
+
updates.forEach((u) => (u.hidden = active !== null && !tagsOf(u).includes(active)));
|
|
68
|
+
buttons.forEach((b) => b.setAttribute('aria-pressed', String((b.dataset.tag ?? null) === active)));
|
|
69
|
+
};
|
|
70
|
+
for (const tag of [null, ...allTags]) {
|
|
71
|
+
const b = document.createElement('button');
|
|
72
|
+
b.type = 'button';
|
|
73
|
+
b.className = 'wd-update-filter';
|
|
74
|
+
b.textContent = tag ?? 'All';
|
|
75
|
+
if (tag !== null) b.dataset.tag = tag;
|
|
76
|
+
b.addEventListener('click', () => {
|
|
77
|
+
active = tag === null || active === tag ? null : tag;
|
|
78
|
+
apply();
|
|
79
|
+
});
|
|
80
|
+
buttons.push(b);
|
|
81
|
+
bar.appendChild(b);
|
|
82
|
+
}
|
|
83
|
+
updates[0].before(bar);
|
|
84
|
+
apply();
|
|
85
|
+
}
|
|
86
|
+
initUpdateFilters(document);
|
|
87
|
+
document.addEventListener('astro:page-load', () => initUpdateFilters(document));
|
|
88
|
+
</script>
|
|
89
|
+
<style>
|
|
90
|
+
.wd-update {
|
|
91
|
+
display: grid;
|
|
92
|
+
grid-template-columns: 11rem minmax(0, 1fr);
|
|
93
|
+
gap: 2rem;
|
|
94
|
+
padding: 2rem 0;
|
|
95
|
+
border-top: 1px solid var(--wd-border);
|
|
96
|
+
}
|
|
97
|
+
.wd-update:first-of-type {
|
|
98
|
+
border-top: 0;
|
|
99
|
+
}
|
|
100
|
+
.wd-update[hidden] {
|
|
101
|
+
display: none;
|
|
102
|
+
}
|
|
103
|
+
.wd-update-meta {
|
|
104
|
+
position: sticky;
|
|
105
|
+
top: calc(var(--wd-topbar-offset, 5rem) + 1rem);
|
|
106
|
+
align-self: start;
|
|
107
|
+
}
|
|
108
|
+
.wd-update-label {
|
|
109
|
+
scroll-margin-top: calc(var(--wd-topbar-offset, 5rem) + 1rem);
|
|
110
|
+
}
|
|
111
|
+
.wd-update-label a {
|
|
112
|
+
display: inline-block;
|
|
113
|
+
padding: 0.2rem 0.6rem;
|
|
114
|
+
border-radius: 999px;
|
|
115
|
+
font-size: 0.85rem;
|
|
116
|
+
font-weight: 600;
|
|
117
|
+
text-decoration: none;
|
|
118
|
+
color: var(--wd-primary);
|
|
119
|
+
background: color-mix(in srgb, var(--wd-primary) 12%, transparent);
|
|
120
|
+
}
|
|
121
|
+
.wd-update-description {
|
|
122
|
+
margin-top: 0.5rem;
|
|
123
|
+
font-size: 0.85rem;
|
|
124
|
+
color: var(--wd-text-muted);
|
|
125
|
+
}
|
|
126
|
+
.wd-update-tags {
|
|
127
|
+
display: flex;
|
|
128
|
+
flex-wrap: wrap;
|
|
129
|
+
gap: 0.3rem;
|
|
130
|
+
margin-top: 0.5rem;
|
|
131
|
+
}
|
|
132
|
+
.wd-update-tag {
|
|
133
|
+
padding: 0.05rem 0.45rem;
|
|
134
|
+
border: 1px solid var(--wd-border);
|
|
135
|
+
border-radius: 999px;
|
|
136
|
+
font-size: 0.72rem;
|
|
137
|
+
color: var(--wd-text-muted);
|
|
138
|
+
}
|
|
139
|
+
.wd-update-body > :global(:first-child) {
|
|
140
|
+
margin-top: 0;
|
|
141
|
+
}
|
|
142
|
+
@media (max-width: 720px) {
|
|
143
|
+
.wd-update {
|
|
144
|
+
grid-template-columns: minmax(0, 1fr);
|
|
145
|
+
gap: 0.75rem;
|
|
146
|
+
}
|
|
147
|
+
.wd-update-meta {
|
|
148
|
+
position: static;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
:global(.wd-update-filters) {
|
|
152
|
+
display: flex;
|
|
153
|
+
flex-wrap: wrap;
|
|
154
|
+
gap: 0.4rem;
|
|
155
|
+
margin: 1rem 0 0.5rem;
|
|
156
|
+
}
|
|
157
|
+
:global(.wd-update-filter) {
|
|
158
|
+
padding: 0.2rem 0.7rem;
|
|
159
|
+
border: 1px solid var(--wd-border);
|
|
160
|
+
border-radius: 999px;
|
|
161
|
+
background: none;
|
|
162
|
+
color: var(--wd-text-muted);
|
|
163
|
+
font-size: 0.8rem;
|
|
164
|
+
cursor: pointer;
|
|
165
|
+
}
|
|
166
|
+
:global(.wd-update-filter[aria-pressed='true']) {
|
|
167
|
+
border-color: var(--wd-primary);
|
|
168
|
+
color: var(--wd-primary);
|
|
169
|
+
background: color-mix(in srgb, var(--wd-primary) 10%, transparent);
|
|
170
|
+
}
|
|
171
|
+
</style>
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Mintlify's <View title="..." icon="..."> - content for one of several
|
|
3
|
+
// alternatives (a language, a framework) on the same page. Every distinct
|
|
4
|
+
// `title` on the page becomes an option in one switcher, placed under the
|
|
5
|
+
// page title (or before the first View on a page without one); only the
|
|
6
|
+
// selected title's Views show. As on Mintlify, the table of contents only
|
|
7
|
+
// lists the headings in the visible Views.
|
|
8
|
+
//
|
|
9
|
+
// The choice is remembered (localStorage) across pages, so a reader who
|
|
10
|
+
// picks "Python" keeps seeing Python wherever the same title exists. A
|
|
11
|
+
// link to an anchor inside a hidden View switches to that View first.
|
|
12
|
+
import AppIcon from './AppIcon.astro';
|
|
13
|
+
interface Props {
|
|
14
|
+
title: string;
|
|
15
|
+
icon?: string;
|
|
16
|
+
}
|
|
17
|
+
const { title, icon } = Astro.props as Props;
|
|
18
|
+
---
|
|
19
|
+
<div class="wd-view" data-view-title={title}>
|
|
20
|
+
{icon && <span class="wd-view-icon-src" hidden><AppIcon icon={icon} class="wd-view-icon" /></span>}
|
|
21
|
+
<slot />
|
|
22
|
+
</div>
|
|
23
|
+
<script>
|
|
24
|
+
const STORAGE_KEY = 'wd-view';
|
|
25
|
+
|
|
26
|
+
function initViews(root: Document) {
|
|
27
|
+
const views = Array.from(root.querySelectorAll<HTMLElement>('.wd-view[data-view-title]'));
|
|
28
|
+
if (views.length === 0 || root.querySelector('.wd-view-switcher')) return;
|
|
29
|
+
|
|
30
|
+
const titles = [...new Set(views.map((v) => v.dataset.viewTitle!))];
|
|
31
|
+
const iconFor = (title: string) =>
|
|
32
|
+
views.find((v) => v.dataset.viewTitle === title)?.querySelector<HTMLElement>('.wd-view-icon-src > *') ?? null;
|
|
33
|
+
|
|
34
|
+
let stored: string | null = null;
|
|
35
|
+
try {
|
|
36
|
+
stored = localStorage.getItem(STORAGE_KEY);
|
|
37
|
+
} catch {}
|
|
38
|
+
const hashTarget = location.hash ? document.getElementById(decodeURIComponent(location.hash.slice(1))) : null;
|
|
39
|
+
const hashView = hashTarget?.closest<HTMLElement>('.wd-view')?.dataset.viewTitle;
|
|
40
|
+
let current = hashView ?? (stored && titles.includes(stored) ? stored : titles[0]);
|
|
41
|
+
|
|
42
|
+
// The switcher: a button showing the current option, and a menu of all.
|
|
43
|
+
const switcher = document.createElement('div');
|
|
44
|
+
switcher.className = 'wd-view-switcher';
|
|
45
|
+
const button = document.createElement('button');
|
|
46
|
+
button.type = 'button';
|
|
47
|
+
button.className = 'wd-view-button';
|
|
48
|
+
button.setAttribute('aria-haspopup', 'listbox');
|
|
49
|
+
button.setAttribute('aria-expanded', 'false');
|
|
50
|
+
const menu = document.createElement('ul');
|
|
51
|
+
menu.className = 'wd-view-menu';
|
|
52
|
+
menu.setAttribute('role', 'listbox');
|
|
53
|
+
menu.hidden = true;
|
|
54
|
+
|
|
55
|
+
const label = (title: string) => {
|
|
56
|
+
const frag = document.createDocumentFragment();
|
|
57
|
+
const icon = iconFor(title);
|
|
58
|
+
if (icon) frag.appendChild(icon.cloneNode(true));
|
|
59
|
+
frag.appendChild(document.createTextNode(title));
|
|
60
|
+
return frag;
|
|
61
|
+
};
|
|
62
|
+
const options = titles.map((title) => {
|
|
63
|
+
const li = document.createElement('li');
|
|
64
|
+
li.setAttribute('role', 'option');
|
|
65
|
+
li.tabIndex = -1;
|
|
66
|
+
li.dataset.viewTitle = title;
|
|
67
|
+
li.appendChild(label(title));
|
|
68
|
+
li.addEventListener('click', () => {
|
|
69
|
+
select(title, true);
|
|
70
|
+
close();
|
|
71
|
+
button.focus();
|
|
72
|
+
});
|
|
73
|
+
li.addEventListener('keydown', (e) => {
|
|
74
|
+
const i = options.indexOf(li);
|
|
75
|
+
if (e.key === 'ArrowDown') options[Math.min(i + 1, options.length - 1)].focus();
|
|
76
|
+
else if (e.key === 'ArrowUp') options[Math.max(i - 1, 0)].focus();
|
|
77
|
+
else if (e.key === 'Enter' || e.key === ' ') li.click();
|
|
78
|
+
else if (e.key === 'Escape') {
|
|
79
|
+
close();
|
|
80
|
+
button.focus();
|
|
81
|
+
} else return;
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
});
|
|
84
|
+
menu.appendChild(li);
|
|
85
|
+
return li;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const open = () => {
|
|
89
|
+
menu.hidden = false;
|
|
90
|
+
button.setAttribute('aria-expanded', 'true');
|
|
91
|
+
(options.find((o) => o.dataset.viewTitle === current) ?? options[0]).focus();
|
|
92
|
+
};
|
|
93
|
+
const close = () => {
|
|
94
|
+
menu.hidden = true;
|
|
95
|
+
button.setAttribute('aria-expanded', 'false');
|
|
96
|
+
};
|
|
97
|
+
button.addEventListener('click', () => (menu.hidden ? open() : close()));
|
|
98
|
+
document.addEventListener('click', (e) => {
|
|
99
|
+
if (!switcher.contains(e.target as Node)) close();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
function select(title: string, remember: boolean) {
|
|
103
|
+
current = title;
|
|
104
|
+
views.forEach((v) => (v.hidden = v.dataset.viewTitle !== title));
|
|
105
|
+
button.replaceChildren(label(title));
|
|
106
|
+
const chevron = document.createElement('span');
|
|
107
|
+
chevron.className = 'wd-view-chevron';
|
|
108
|
+
chevron.setAttribute('aria-hidden', 'true');
|
|
109
|
+
button.appendChild(chevron);
|
|
110
|
+
options.forEach((o) => o.setAttribute('aria-selected', String(o.dataset.viewTitle === title)));
|
|
111
|
+
// Table of contents: only headings the reader can currently see.
|
|
112
|
+
document.querySelectorAll<HTMLAnchorElement>('.wd-toc a[data-toc-slug]').forEach((a) => {
|
|
113
|
+
const heading = document.getElementById(a.dataset.tocSlug ?? '');
|
|
114
|
+
const li = a.closest('li');
|
|
115
|
+
if (li) li.hidden = Boolean(heading?.closest('.wd-view[hidden]'));
|
|
116
|
+
});
|
|
117
|
+
if (remember) {
|
|
118
|
+
try {
|
|
119
|
+
localStorage.setItem(STORAGE_KEY, title);
|
|
120
|
+
} catch {}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
switcher.append(button, menu);
|
|
125
|
+
const header = document.querySelector('.wd-article-header');
|
|
126
|
+
if (header) header.after(switcher);
|
|
127
|
+
else views[0].before(switcher);
|
|
128
|
+
select(current, false);
|
|
129
|
+
if (hashTarget && hashView) hashTarget.scrollIntoView();
|
|
130
|
+
|
|
131
|
+
// Same for an in-page link to an anchor inside a hidden View.
|
|
132
|
+
window.addEventListener('hashchange', () => {
|
|
133
|
+
if (!switcher.isConnected) return;
|
|
134
|
+
const el = location.hash ? document.getElementById(decodeURIComponent(location.hash.slice(1))) : null;
|
|
135
|
+
const title = el?.closest<HTMLElement>('.wd-view[hidden]')?.dataset.viewTitle;
|
|
136
|
+
if (!title) return;
|
|
137
|
+
select(title, false);
|
|
138
|
+
el!.scrollIntoView();
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
initViews(document);
|
|
142
|
+
document.addEventListener('astro:page-load', () => initViews(document));
|
|
143
|
+
</script>
|
|
144
|
+
<style>
|
|
145
|
+
.wd-view[hidden] {
|
|
146
|
+
display: none;
|
|
147
|
+
}
|
|
148
|
+
:global(.wd-view-switcher) {
|
|
149
|
+
position: relative;
|
|
150
|
+
display: inline-block;
|
|
151
|
+
margin: 0 0 1rem;
|
|
152
|
+
}
|
|
153
|
+
:global(.wd-view-button) {
|
|
154
|
+
display: inline-flex;
|
|
155
|
+
align-items: center;
|
|
156
|
+
gap: 0.45rem;
|
|
157
|
+
padding: 0.35rem 0.75rem;
|
|
158
|
+
border: 1px solid var(--wd-border);
|
|
159
|
+
border-radius: 0.5rem;
|
|
160
|
+
background: var(--wd-background);
|
|
161
|
+
color: var(--wd-text);
|
|
162
|
+
font-size: 0.875rem;
|
|
163
|
+
cursor: pointer;
|
|
164
|
+
}
|
|
165
|
+
:global(.wd-view-button:hover) {
|
|
166
|
+
border-color: var(--wd-primary);
|
|
167
|
+
}
|
|
168
|
+
:global(.wd-view-chevron) {
|
|
169
|
+
width: 0.4rem;
|
|
170
|
+
height: 0.4rem;
|
|
171
|
+
margin-left: 0.2rem;
|
|
172
|
+
border-right: 1.5px solid currentColor;
|
|
173
|
+
border-bottom: 1.5px solid currentColor;
|
|
174
|
+
transform: translateY(-2px) rotate(45deg);
|
|
175
|
+
}
|
|
176
|
+
:global(.wd-view-menu) {
|
|
177
|
+
position: absolute;
|
|
178
|
+
top: calc(100% + 0.3rem);
|
|
179
|
+
left: 0;
|
|
180
|
+
z-index: 20;
|
|
181
|
+
min-width: 100%;
|
|
182
|
+
margin: 0;
|
|
183
|
+
padding: 0.3rem;
|
|
184
|
+
list-style: none;
|
|
185
|
+
border: 1px solid var(--wd-border);
|
|
186
|
+
border-radius: 0.5rem;
|
|
187
|
+
background: var(--wd-background);
|
|
188
|
+
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
|
|
189
|
+
}
|
|
190
|
+
:global(.wd-view-menu li) {
|
|
191
|
+
display: flex;
|
|
192
|
+
align-items: center;
|
|
193
|
+
gap: 0.45rem;
|
|
194
|
+
margin: 0;
|
|
195
|
+
padding: 0.35rem 0.6rem;
|
|
196
|
+
border-radius: 0.35rem;
|
|
197
|
+
font-size: 0.875rem;
|
|
198
|
+
white-space: nowrap;
|
|
199
|
+
cursor: pointer;
|
|
200
|
+
}
|
|
201
|
+
:global(.wd-view-menu li:hover),
|
|
202
|
+
:global(.wd-view-menu li:focus),
|
|
203
|
+
:global(.wd-view-menu li[aria-selected='true']) {
|
|
204
|
+
outline: none;
|
|
205
|
+
background: color-mix(in srgb, var(--wd-primary) 10%, transparent);
|
|
206
|
+
color: var(--wd-primary);
|
|
207
|
+
}
|
|
208
|
+
:global(svg.wd-view-icon),
|
|
209
|
+
:global(span.wd-view-icon) {
|
|
210
|
+
width: 1em;
|
|
211
|
+
height: 1em;
|
|
212
|
+
flex-shrink: 0;
|
|
213
|
+
}
|
|
214
|
+
</style>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Mintlify's <Visibility for="humans|agents">. On the rendered site,
|
|
3
|
+
// `humans` content shows and `agents` content doesn't; the per-page `.md`
|
|
4
|
+
// route and llms-full.txt do the opposite (see lib/visibility.js). A
|
|
5
|
+
// <Visibility> with no `for` counts as `humans`.
|
|
6
|
+
interface Props {
|
|
7
|
+
for?: 'humans' | 'agents';
|
|
8
|
+
}
|
|
9
|
+
const { for: audience = 'humans' } = Astro.props as Props;
|
|
10
|
+
---
|
|
11
|
+
{audience !== 'agents' && <slot />}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Mintlify's dotted component names - <Tree.Folder>, <Color.Item>,
|
|
2
|
+
// <GitHub.Repo>, ... MDX compiles <Tree.Folder> to a property lookup on
|
|
3
|
+
// whatever `Tree` resolves to, so the parts are attached as properties of
|
|
4
|
+
// their parent here, once, and everything that hands components to MDX (the
|
|
5
|
+
// src/components/index.ts barrel and [...slug].astro's `components` map)
|
|
6
|
+
// takes these three from this module rather than importing the .astro files
|
|
7
|
+
// directly. `FileTree` is Mintlify's other name for `Tree`.
|
|
8
|
+
import TreeRoot from './Tree.astro';
|
|
9
|
+
import TreeFolder from './TreeFolder.astro';
|
|
10
|
+
import TreeFile from './TreeFile.astro';
|
|
11
|
+
import ColorRoot from './Color.astro';
|
|
12
|
+
import ColorRow from './ColorRow.astro';
|
|
13
|
+
import ColorItem from './ColorItem.astro';
|
|
14
|
+
import GitHubRepo from './GitHubRepo.astro';
|
|
15
|
+
|
|
16
|
+
export const Tree = Object.assign(TreeRoot, { Folder: TreeFolder, File: TreeFile });
|
|
17
|
+
export const FileTree = Tree;
|
|
18
|
+
export const Color = Object.assign(ColorRoot, { Row: ColorRow, Item: ColorItem });
|
|
19
|
+
// Mintlify has no bare <GitHub> - only <GitHub.Repo> - so this is just a
|
|
20
|
+
// namespace object, not a component.
|
|
21
|
+
export const GitHub = { Repo: GitHubRepo };
|
package/src/components/index.ts
CHANGED
|
@@ -51,3 +51,10 @@ 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
53
|
export { default as Tooltip } from './Tooltip.astro';
|
|
54
|
+
export { default as Update } from './Update.astro';
|
|
55
|
+
export { default as Tile } from './Tile.astro';
|
|
56
|
+
export { default as Panel } from './Panel.astro';
|
|
57
|
+
export { default as Prompt } from './Prompt.astro';
|
|
58
|
+
export { default as View } from './View.astro';
|
|
59
|
+
export { default as Visibility } from './Visibility.astro';
|
|
60
|
+
export { Tree, FileTree, Color, GitHub } from './compound';
|
package/src/content.config.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
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 {
|
|
7
|
+
import { pageFrontmatterSchema, findAllPages } from './lib/config';
|
|
9
8
|
import { writedocsTempDir } from './lib/writedocs-temp-dir.js';
|
|
10
9
|
|
|
11
10
|
const contentDir = process.env.WRITEDOCS_CONTENT_DIR || process.cwd();
|
|
@@ -32,131 +31,11 @@ const contentDir = process.env.WRITEDOCS_CONTENT_DIR || process.cwd();
|
|
|
32
31
|
const generatedDocsDir = path.join(writedocsTempDir(contentDir), 'generated-docs');
|
|
33
32
|
const generatedDocsBase = pathToFileURL(generatedDocsDir + path.sep);
|
|
34
33
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
);
|
|
34
|
+
// Page frontmatter - defined in lib/config-schema.ts as
|
|
35
|
+
// pageFrontmatterSchema, next to writedocs.json's own schema, so
|
|
36
|
+
// `writedocs validate` checks every page against exactly the schema the
|
|
37
|
+
// build uses (that file compiles to the plain JS validate loads).
|
|
38
|
+
const docsSchema = pageFrontmatterSchema;
|
|
160
39
|
|
|
161
40
|
/** Wraps another loader so it's skipped entirely - no filesystem scan, no
|
|
162
41
|
* "directory doesn't exist"/"no files found" warning - when its own base
|