Skip to content

Commit b77a534

Browse files
authored
docs: improve nav maintainability, improve nav structure, begin build out of key content sections (#10519)
* restructure guides flow * rework how nav is structured * begin work on reactivity guide * build out reactivity docs * build out reactivity docs
1 parent 0612f90 commit b77a534

104 files changed

Lines changed: 819 additions & 1245 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs-viewer/src/site-utils.ts

Lines changed: 112 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -19,94 +19,88 @@ function segmentToTitle(segment: string, prevSegment: string | null) {
1919
return result === 'Index' ? 'Introduction' : result;
2020
}
2121

22+
interface DirMeta {
23+
title?: string;
24+
collapsed?: boolean;
25+
draft?: boolean;
26+
/** Ordered list of child slugs (filenames without .md, or directory names). Unlisted items sort alphabetically after listed ones. */
27+
items?: string[];
28+
}
29+
2230
interface WarpDriveFrontMatter {
23-
categoryTitle?: string;
2431
title?: string;
25-
categoryOrder?: number;
26-
order?: number;
2732
draft?: boolean;
28-
collapsed?: boolean;
2933
}
34+
3035
interface GuideGroup {
31-
/**
32-
* The Text To Display
33-
*/
3436
text: string;
35-
/**
36-
* The Path For This group
37-
* "On Disc".
38-
*/
3937
path: string;
40-
/**
41-
* The URL Slug For This group
42-
* if different from the path.
43-
*
44-
* This is currently unused but is set
45-
* by the frontmatter of an `index.md` file
46-
* in the directory.
47-
*/
4838
slug: string;
49-
/**
50-
* This will be the categoryIndex specified by
51-
* the frontmatter of an `index.md` file in the directory.
52-
*
53-
* Else it will be set to the next open index available
54-
* once "known" indeces have been assigned.
55-
*/
56-
index: number | null;
57-
/**
58-
* Whether the directory should default to open or closed.
59-
*
60-
* This is set by the frontmatter of an `index.md` file in the directory.
61-
* else by config above in this file, and defaults to `true`.
62-
*/
39+
/** Items list from this directory's _meta.json, used to sort this group's children. */
40+
orderedItems?: string[];
6341
collapsed: boolean | null;
64-
/**
65-
* The child items/groups of this group, if any.
66-
*/
6742
items: Record<string, GuideGroup>;
68-
/**
69-
*
70-
*/
7143
link?: string;
7244
}
7345

46+
function normPath(p: string): string {
47+
return p.split(path.sep).join('/');
48+
}
49+
7450
export async function getGuidesStructure() {
7551
const GuidesDirectoryPath = path.join(__dirname, '../docs.warp-drive.io/guides');
52+
53+
// Load all _meta.json files up front; keys are forward-slash dir paths relative to GuidesDirectoryPath
54+
const metaFiles = globSync('**/_meta.json', { cwd: GuidesDirectoryPath });
55+
const dirMeta = new Map<string, DirMeta>();
56+
for (const metaFile of metaFiles) {
57+
const dirPath = path.dirname(metaFile);
58+
const key = dirPath === '.' ? '' : normPath(dirPath);
59+
dirMeta.set(key, JSON.parse(readFileSync(path.join(GuidesDirectoryPath, metaFile), 'utf-8')) as DirMeta);
60+
}
61+
7662
const glob = globSync('**/*.md', { cwd: GuidesDirectoryPath });
7763
const groups: Record<string, GuideGroup> = {};
7864

7965
for (const filepath of glob) {
80-
const slugPath = [];
66+
const slugPath: string[] = [];
8167
const text = readFileSync(path.join(GuidesDirectoryPath, filepath), 'utf-8');
8268
const frontMatter = fm<WarpDriveFrontMatter>(text);
8369

8470
if (frontMatter.attributes.draft) {
85-
// skip hidden files
71+
continue;
72+
}
73+
74+
// Skip files whose immediate parent directory is marked draft in _meta.json
75+
const fileDir = normPath(path.dirname(filepath));
76+
if (fileDir !== '.' && dirMeta.get(fileDir)?.draft) {
8677
continue;
8778
}
8879

8980
if (filepath === 'index.md') {
90-
groups['the-manual'] = groups['the-manual'] || {
91-
text: frontMatter.attributes.categoryTitle!,
92-
path: filepath,
93-
slug: filepath,
94-
index: frontMatter.attributes.categoryOrder || 0,
95-
collapsed: frontMatter.attributes.collapsed || true,
81+
const rootMeta = dirMeta.get('') ?? {};
82+
const theManualMeta = dirMeta.get('the-manual') ?? {};
83+
groups['the-manual'] = groups['the-manual'] ?? {
84+
text: rootMeta.title ?? 'The Manual',
85+
path: 'the-manual',
86+
slug: 'the-manual',
87+
orderedItems: theManualMeta.items,
88+
collapsed: rootMeta.collapsed ?? true,
9689
link: '/guides/index.md',
9790
items: {},
9891
};
9992
Object.assign(groups['the-manual'], {
100-
text: frontMatter.attributes.categoryTitle!,
101-
index: frontMatter.attributes.categoryOrder || 0,
102-
collapsed: frontMatter.attributes.collapsed || true,
93+
text: rootMeta.title ?? 'The Manual',
94+
path: 'the-manual',
95+
slug: 'the-manual',
96+
orderedItems: theManualMeta.items,
97+
collapsed: rootMeta.collapsed ?? true,
10398
link: '/guides/index.md',
10499
});
105-
groups['the-manual'].items[filepath] = {
106-
text: frontMatter.attributes.title!,
107-
path: filepath,
108-
slug: filepath,
109-
index: frontMatter.attributes.order ?? 0,
100+
groups['the-manual'].items['index.md'] = {
101+
text: frontMatter.attributes.title ?? 'Introduction',
102+
path: 'index.md',
103+
slug: 'index.md',
110104
collapsed: false,
111105
items: {},
112106
link: '/guides/index.md',
@@ -119,7 +113,6 @@ export async function getGuidesStructure() {
119113
let isIndex = false;
120114

121115
if (lastSegment === 'index.md') {
122-
// we treat index files as the main entry to any guides directory
123116
lastSegment = segments.pop()!;
124117

125118
if (!lastSegment) {
@@ -130,22 +123,27 @@ export async function getGuidesStructure() {
130123
}
131124

132125
let group = groups;
133-
let parent = null;
126+
let parent: GuideGroup | null = null;
134127

135-
// build out nodes for each segment
136-
// if there is not one yet.
137128
for (let i = 0; i < segments.length; i++) {
138129
const prevSegment = i > 0 ? segments[i - 1] : null;
139130
const segment = segments[i];
140131
slugPath.push(segment);
141132
const key = slugPath.join('.');
142-
const collapsed = AlwaysOpenGroups.includes(key) ? null : DefaultOpenGroups.includes(key) ? false : true;
133+
const segmentMeta = dirMeta.get(normPath(slugPath.join(path.sep))) ?? {};
134+
const collapsed =
135+
segmentMeta.collapsed !== undefined
136+
? segmentMeta.collapsed
137+
: AlwaysOpenGroups.includes(key)
138+
? null
139+
: DefaultOpenGroups.includes(key)
140+
? false
141+
: true;
143142

144-
// setup a nested segment if we don't already have one
145143
if (!group[segment]) {
146144
group[segment] = {
147-
text: segmentToTitle(segment, prevSegment),
148-
index: null,
145+
text: segmentMeta.title ?? segmentToTitle(segment, prevSegment),
146+
orderedItems: segmentMeta.items,
149147
path: segment,
150148
slug: segment,
151149
collapsed,
@@ -161,71 +159,58 @@ export async function getGuidesStructure() {
161159
const key = slugPath.join('.');
162160
const realUrl = `/guides/${filepath}`;
163161

164-
// setup our leaf-most segment for this file
165-
// if needed, it may exist from a child directory already
166162
if (!group[lastSegment]) {
163+
const leafMeta = dirMeta.get(normPath(slugPath.join(path.sep))) ?? {};
167164
group[lastSegment] = {
168-
text: segmentToTitle(lastSegment, parent ? parent.path : null),
169-
index: null,
165+
text: leafMeta.title ?? segmentToTitle(lastSegment, parent ? parent.path : null),
166+
orderedItems: leafMeta.items,
170167
path: lastSegment,
171168
slug: lastSegment,
172-
collapsed: AlwaysOpenGroups.includes(key) ? null : DefaultOpenGroups.includes(key) ? false : true,
169+
collapsed:
170+
leafMeta.collapsed !== undefined
171+
? leafMeta.collapsed
172+
: AlwaysOpenGroups.includes(key)
173+
? null
174+
: DefaultOpenGroups.includes(key)
175+
? false
176+
: true,
173177
items: {},
174-
// if we are an index file, this has the effect of setting the link on the parent node
175-
// this seems to work even though there's an issue
176-
// that says it doesn't: https://github.com/vuejs/vitepress/issues/2989
177-
// however:
178-
// when doing this, the "next page" feature breaks for
179-
// these pages, so for now we just do non-clickable headers.
180178
link: realUrl,
181179
};
182180
} else {
183-
// the segment was previously generated from a file in a child directory on the same path.
184-
// we need to add in the link.
185181
group[lastSegment].link = realUrl;
186182
}
187183

188-
// update the leaf-most segment with any frontmatter info
189184
const leaf = group[lastSegment]!;
190185

191-
// if the leaf is the index, we need to update the category entry
192-
// and then generate an item entry for it.
193186
if (isIndex) {
194-
if ('collapsed' in frontMatter.attributes) {
195-
leaf.collapsed = frontMatter.attributes.collapsed!;
196-
}
197-
if ('categoryOrder' in frontMatter.attributes) {
198-
leaf.index = frontMatter.attributes.categoryOrder!;
199-
}
200-
if ('categoryTitle' in frontMatter.attributes) {
201-
leaf.text = frontMatter.attributes.categoryTitle!;
202-
}
187+
const leafDirPath = normPath(slugPath.join(path.sep));
188+
const leafMeta = dirMeta.get(leafDirPath) ?? {};
189+
190+
if (leafMeta.draft) continue;
191+
192+
// _meta.json is authoritative for category metadata; always apply it
193+
if (leafMeta.title !== undefined) leaf.text = leafMeta.title;
194+
if (leafMeta.collapsed !== undefined) leaf.collapsed = leafMeta.collapsed;
195+
leaf.orderedItems = leafMeta.items;
203196

204-
// generate the entry for the file itself unless we are a top-level index file
205197
leaf.items['index.md'] = {
206198
path: 'index.md',
207199
slug: 'index.md',
208200
collapsed: false,
209201
text: frontMatter.attributes.title ?? 'Overview',
210-
index: frontMatter.attributes.order ?? 0,
211-
link: group[lastSegment]!.link,
202+
link: group[lastSegment]!.link!,
212203
items: {},
213204
};
214205
} else {
215-
// update the leaf's title and order
216206
if (frontMatter.attributes.title) {
217207
leaf.text = frontMatter.attributes.title;
218208
}
219-
if ('order' in frontMatter.attributes) {
220-
leaf.index = frontMatter.attributes.order!;
221-
}
222209
}
223210
}
224211

225-
// deep iterate converting items objects to arrays
226-
const result = deepConvert(groups);
227-
// console.log(JSON.stringify(result, null, 2));
228-
// console.log(JSON.stringify(rewritten, null, 2));
212+
const rootMeta = dirMeta.get('') ?? {};
213+
const result = deepConvert(groups, rootMeta.items);
229214
const structure = { paths: result };
230215

231216
writeFileSync(
@@ -240,50 +225,51 @@ export async function getGuidesStructure() {
240225
return { paths: result };
241226
}
242227

243-
function deepConvert(obj: Record<string, any>) {
228+
function deepConvert(obj: Record<string, any>, orderedItems?: string[]) {
244229
const groups = Array.from(Object.values(obj));
245-
const sortedGroups = new Array(groups.length).fill(null);
246230

247231
for (const group of groups) {
248-
if (group.index !== null) {
249-
if (group.index < 0 || group.index >= groups.length) {
250-
throw new Error(`Invalid index ${group.index} for ${group.path}, must be between 0 and ${groups.length - 1}`);
251-
}
252-
if (sortedGroups[group.index] !== null) {
253-
throw new Error(`Duplicate index ${group.index} for ${group.path}, matches ${sortedGroups[group.index]}`);
254-
}
255-
sortedGroups[group.index] = group;
256-
}
257-
258-
delete group.path;
259-
delete group.slug;
260-
261232
if (group.items) {
262233
if (Object.keys(group.items).length === 0) {
263234
delete group.items;
264235
delete group.collapsed;
265236
} else {
266-
group.items = deepConvert(group.items);
237+
// Each group carries orderedItems from its own _meta.json for sorting its children
238+
group.items = deepConvert(group.items, group.orderedItems);
267239

268-
if (!group.link && !group.items[0].items) {
269-
group.link = group.items[0].link;
240+
if (!group.link && !group.items[0]?.items) {
241+
group.link = group.items[0]?.link;
270242
}
271243
}
272244
}
245+
delete group.orderedItems;
273246
}
274247

275-
for (const group of groups) {
276-
if (group.index === null) {
277-
// find the first null index and insert
278-
const firstNullIndex = sortedGroups.findIndex((g) => g === null);
279-
if (firstNullIndex !== -1) {
280-
sortedGroups[firstNullIndex] = group;
281-
group.index = firstNullIndex;
282-
}
248+
// index.md synthetic entries always first; otherwise sort by orderedItems list, then alphabetically
249+
groups.sort((a, b) => {
250+
if (a.slug === 'index.md') return -1;
251+
if (b.slug === 'index.md') return 1;
252+
253+
if (orderedItems?.length) {
254+
const aKey = (a.slug ?? '').replace(/\.md$/, '');
255+
const bKey = (b.slug ?? '').replace(/\.md$/, '');
256+
const aIdx = orderedItems.indexOf(aKey);
257+
const bIdx = orderedItems.indexOf(bKey);
258+
if (aIdx === -1 && bIdx === -1) return (a.text ?? '').localeCompare(b.text ?? '');
259+
if (aIdx === -1) return 1;
260+
if (bIdx === -1) return -1;
261+
return aIdx - bIdx;
283262
}
263+
264+
return (a.text ?? '').localeCompare(b.text ?? '');
265+
});
266+
267+
for (const group of groups) {
268+
delete group.path;
269+
delete group.slug;
284270
}
285271

286-
return sortedGroups;
272+
return groups;
287273
}
288274

289275
type SidebarItem = { text: string; items?: SidebarItem[]; link?: string; collapsed?: boolean };

guides/_meta.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"title": "The Manual",
3+
"collapsed": true,
4+
"items": ["installation", "configuration", "the-manual", "linting", "migrating", "contributing"]
5+
}

guides/configuration/_meta.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"title": "Configuration",
3+
"items": ["advanced", "ember", "legacy-package-setup"]
4+
}

guides/configuration/advanced.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
---
2-
order: 1
32
title: Setup - Advanced
43
---
54

guides/configuration/ember.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
---
2-
order: 2
32
title: Setup - Legacy (Ember)
43
---
54

guides/configuration/index.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
---
22
title: 'Setup'
3-
categoryOrder: 1
43
outline:
54
level: 2,3
65
---
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"title": "Older Packages (Ember Only)"
3+
}

0 commit comments

Comments
 (0)