blume 0.6.7 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +618 -0
- package/LICENSE +21 -0
- package/README.md +107 -0
- package/dist/cli/index.js +2609 -1041
- package/dist/cli/index.js.map +110 -103
- package/dist/types/ai/component-markdown.d.ts +34 -0
- package/dist/types/components/content/youtube.d.ts +18 -0
- package/dist/types/core/base-path.d.ts +47 -0
- package/dist/types/core/config-input.d.ts +110 -12
- package/dist/types/core/config.d.ts +6 -4
- package/dist/types/core/data.d.ts +4 -0
- package/dist/types/core/i18n-ui.d.ts +477 -135
- package/dist/types/core/schema.d.ts +309 -195
- package/dist/types/core/sources/types.d.ts +2 -0
- package/dist/types/core/types.d.ts +6 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/openapi/references.d.ts +60 -0
- package/docs/01-quickstart.mdx +5 -2
- package/docs/02-deployment.mdx +24 -9
- package/docs/03-faq.mdx +46 -16
- package/docs/advanced/custom-pages.mdx +1 -1
- package/docs/advanced/skills.mdx +1 -1
- package/docs/configuration/ai.mdx +49 -10
- package/docs/configuration/customization.mdx +11 -0
- package/docs/configuration/index.mdx +33 -3
- package/docs/configuration/seo.mdx +2 -2
- package/docs/content/components.mdx +30 -3
- package/docs/content/i18n.mdx +1 -1
- package/docs/content/islands.mdx +8 -0
- package/docs/content/navigation.mdx +3 -3
- package/docs/content/sources.mdx +1 -1
- package/docs/content/syntax.mdx +17 -2
- package/docs/index.mdx +2 -2
- package/docs/reference/cli.mdx +8 -6
- package/package.json +15 -4
- package/skills/blume/SKILL.md +5 -3
- package/skills/blume-update-docs/SKILL.md +3 -2
- package/src/ai/agent-readability.ts +11 -5
- package/src/ai/ask-context.ts +7 -2
- package/src/ai/ask-data.ts +3 -0
- package/src/ai/ask.ts +12 -7
- package/src/ai/component-markdown.ts +461 -0
- package/src/ai/llms.ts +143 -23
- package/src/ai/markdown.ts +35 -6
- package/src/ai/mcp/data.ts +33 -8
- package/src/ai/mcp/discovery.ts +10 -3
- package/src/ai/mcp/server.ts +24 -7
- package/src/ai/visibility.ts +74 -0
- package/src/astro/component-slots.ts +16 -4
- package/src/astro/examples.ts +12 -7
- package/src/astro/generate.ts +393 -189
- package/src/astro/index.ts +5 -1
- package/src/astro/integration.ts +9 -5
- package/src/astro/islands.ts +11 -5
- package/src/astro/markdown-negotiation.ts +2 -2
- package/src/astro/pages.ts +89 -22
- package/src/astro/templates.ts +259 -25
- package/src/blume-modules.d.ts +8 -0
- package/src/cli/commands/build.ts +131 -38
- package/src/cli/commands/check.ts +1 -1
- package/src/cli/commands/dev.ts +71 -17
- package/src/cli/commands/doctor.ts +2 -2
- package/src/cli/commands/eject.ts +47 -19
- package/src/cli/commands/init.ts +120 -180
- package/src/cli/commands/preview.ts +4 -1
- package/src/cli/commands/validate.ts +44 -2
- package/src/cli/dev-lock.ts +34 -19
- package/src/cli/eject-scripts.ts +72 -0
- package/src/cli/env.ts +15 -5
- package/src/cli/init/questions.ts +158 -0
- package/src/cli/init/scaffold.ts +380 -0
- package/src/cli/required-secrets.ts +2 -1
- package/src/components/content/AccordionItem.astro +23 -4
- package/src/components/content/Badge.astro +3 -1
- package/src/components/content/Card.astro +4 -2
- package/src/components/content/CodeBlock.astro +3 -0
- package/src/components/content/Component.astro +30 -16
- package/src/components/content/Diff.astro +3 -1
- package/src/components/content/Step.astro +10 -1
- package/src/components/content/Tabs.astro +15 -3
- package/src/components/content/Tile.astro +2 -1
- package/src/components/content/Tooltip.astro +3 -1
- package/src/components/content/Update.astro +9 -2
- package/src/components/content/auto-type-table.ts +25 -9
- package/src/components/content/base-href.ts +33 -0
- package/src/components/content/changelog-element.ts +9 -2
- package/src/components/content/diff.ts +12 -6
- package/src/components/content/mermaid-element.ts +10 -2
- package/src/components/index.ts +23 -1
- package/src/components/islands/AskAI.astro +5 -2
- package/src/components/islands/ask-ai.tsx +68 -12
- package/src/components/islands/base-path.ts +28 -0
- package/src/components/islands/hooks.ts +44 -9
- package/src/components/layout/Banner.astro +12 -3
- package/src/components/layout/Breadcrumbs.astro +2 -1
- package/src/components/layout/Favicon.astro +3 -2
- package/src/components/layout/Header.astro +15 -5
- package/src/components/layout/LanguageSwitcher.astro +2 -1
- package/src/components/layout/Logo.astro +13 -4
- package/src/components/layout/NavSelector.astro +2 -1
- package/src/components/layout/NavTree.astro +22 -7
- package/src/components/layout/PageActions.astro +25 -10
- package/src/components/layout/PageFeedback.astro +4 -1
- package/src/components/layout/PageLayout.astro +51 -9
- package/src/components/layout/Pagination.astro +3 -2
- package/src/components/layout/ReferenceLayout.astro +8 -1
- package/src/components/layout/RootLayout.astro +74 -13
- package/src/components/layout/Search.astro +107 -27
- package/src/components/layout/nav-utils.ts +18 -10
- package/src/components/layout/search/algolia.ts +11 -2
- package/src/components/layout/search/endpoint.ts +11 -5
- package/src/components/layout/search/orama-cloud.ts +8 -2
- package/src/components/layout/search/pagefind.ts +3 -0
- package/src/components/layout/search/types.ts +5 -1
- package/src/components/layout/search/typesense.ts +4 -1
- package/src/components/layout/toc-element.ts +8 -2
- package/src/components/openapi/ApiTagOperations.astro +2 -1
- package/src/components/openapi/Operation.astro +47 -40
- package/src/components/openapi/RequestPanel.astro +8 -2
- package/src/components/openapi/helpers.ts +71 -3
- package/src/components/openapi/panel.ts +1 -1
- package/src/components/openapi/snippets.ts +25 -11
- package/src/core/base-path.ts +94 -0
- package/src/core/builtin-tags.ts +2 -0
- package/src/core/component-overrides.ts +103 -74
- package/src/core/config-input.ts +118 -17
- package/src/core/config.ts +8 -5
- package/src/core/content.ts +2 -0
- package/src/core/data.ts +4 -0
- package/src/core/diagnostics.ts +54 -34
- package/src/core/gitignore.ts +4 -1
- package/src/core/graph.ts +166 -88
- package/src/core/i18n-ui.ts +63 -3
- package/src/core/last-modified.ts +15 -6
- package/src/core/links.ts +69 -25
- package/src/core/manifest.ts +62 -45
- package/src/core/nav-diagnostics.ts +1 -1
- package/src/core/navigation.ts +144 -58
- package/src/core/package-json.ts +17 -2
- package/src/core/project-graph.ts +25 -15
- package/src/core/schema.ts +605 -620
- package/src/core/sources/assets.ts +6 -1
- package/src/core/sources/filesystem.ts +4 -0
- package/src/core/sources/github-releases.ts +2 -1
- package/src/core/sources/mdx-remote.ts +76 -63
- package/src/core/sources/normalize.ts +236 -91
- package/src/core/sources/notion.ts +27 -18
- package/src/core/sources/types.ts +2 -0
- package/src/core/tsconfig-aliases.ts +59 -30
- package/src/core/types.ts +6 -1
- package/src/core/ui-packs/ar.ts +1 -0
- package/src/core/ui-packs/bg.ts +1 -0
- package/src/core/ui-packs/bn.ts +1 -0
- package/src/core/ui-packs/ca.ts +1 -0
- package/src/core/ui-packs/cs.ts +1 -0
- package/src/core/ui-packs/da.ts +1 -0
- package/src/core/ui-packs/de.ts +1 -0
- package/src/core/ui-packs/el.ts +1 -0
- package/src/core/ui-packs/es.ts +1 -0
- package/src/core/ui-packs/fa.ts +1 -0
- package/src/core/ui-packs/fi.ts +1 -0
- package/src/core/ui-packs/fr.ts +2 -1
- package/src/core/ui-packs/he.ts +1 -0
- package/src/core/ui-packs/hi.ts +1 -0
- package/src/core/ui-packs/hr.ts +1 -0
- package/src/core/ui-packs/hu.ts +1 -0
- package/src/core/ui-packs/id.ts +1 -0
- package/src/core/ui-packs/it.ts +1 -0
- package/src/core/ui-packs/ja.ts +1 -0
- package/src/core/ui-packs/ko.ts +1 -0
- package/src/core/ui-packs/nl.ts +1 -0
- package/src/core/ui-packs/no.ts +1 -0
- package/src/core/ui-packs/pl.ts +1 -0
- package/src/core/ui-packs/pt-br.ts +1 -0
- package/src/core/ui-packs/pt.ts +1 -0
- package/src/core/ui-packs/ro.ts +1 -0
- package/src/core/ui-packs/ru.ts +1 -0
- package/src/core/ui-packs/sk.ts +1 -0
- package/src/core/ui-packs/sr.ts +1 -0
- package/src/core/ui-packs/sv.ts +1 -0
- package/src/core/ui-packs/th.ts +1 -0
- package/src/core/ui-packs/tr.ts +1 -0
- package/src/core/ui-packs/uk.ts +1 -0
- package/src/core/ui-packs/vi.ts +1 -0
- package/src/core/ui-packs/zh-tw.ts +1 -0
- package/src/core/ui-packs/zh.ts +1 -0
- package/src/deploy/adapter-output.ts +18 -8
- package/src/deploy/redirects.ts +25 -2
- package/src/deploy/robots.ts +6 -1
- package/src/deploy/rss.ts +10 -3
- package/src/deploy/sitemap.ts +59 -13
- package/src/index.ts +5 -0
- package/src/markdown/base-links.ts +60 -0
- package/src/markdown/code-title.ts +11 -14
- package/src/markdown/index.ts +46 -9
- package/src/markdown/inline-code.ts +14 -4
- package/src/markdown/package-commands.ts +10 -4
- package/src/markdown/themes.ts +24 -0
- package/src/openapi/model.ts +15 -5
- package/src/openapi/parse.ts +21 -0
- package/src/openapi/references.ts +75 -21
- package/src/openapi/render-mdx.ts +11 -6
- package/src/openapi/scalar.ts +32 -16
- package/src/openapi/source.ts +59 -10
- package/src/registry/eject.ts +247 -19
- package/src/registry/registry.ts +0 -3
- package/src/search/build.ts +3 -0
- package/src/search/documents.ts +36 -4
- package/src/search/sync/typesense.ts +6 -4
- package/src/seo/jsonld.ts +28 -17
- package/src/theme/entry.ts +85 -20
package/src/core/manifest.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withBasePath } from "./base-path.ts";
|
|
1
2
|
import { localizeRoute, resolveFallbackLocale } from "./i18n.ts";
|
|
2
3
|
import type { ResolvedConfig } from "./schema.ts";
|
|
3
4
|
import type {
|
|
@@ -26,6 +27,64 @@ export const contentIndexable = (
|
|
|
26
27
|
!page.meta.search.exclude &&
|
|
27
28
|
(!page.meta.sidebar.hidden || config.search.indexing.includeHiddenPages);
|
|
28
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Fallback materialization: render the fallback locale's content at the
|
|
32
|
+
* localized URL for any translation a non-default locale is missing, so static
|
|
33
|
+
* output is fully prerendered (render-fallback, no client redirect). Fallback
|
|
34
|
+
* routes are not indexed and carry no `hreflang` of their own.
|
|
35
|
+
*/
|
|
36
|
+
const buildFallbackRoutes = (
|
|
37
|
+
graph: ContentGraph,
|
|
38
|
+
i18n: NonNullable<ResolvedConfig["i18n"]>,
|
|
39
|
+
alternatesByKey: Map<string, RouteAlternate[]>,
|
|
40
|
+
basePath: string
|
|
41
|
+
): RouteManifestEntry[] => {
|
|
42
|
+
const fallback = resolveFallbackLocale(i18n);
|
|
43
|
+
if (!fallback) {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
const fallbackPages = new Map(
|
|
47
|
+
graph.pages.flatMap((page) =>
|
|
48
|
+
page.locale === fallback ? [[page.translationKey, page] as const] : []
|
|
49
|
+
)
|
|
50
|
+
);
|
|
51
|
+
const routes: RouteManifestEntry[] = [];
|
|
52
|
+
for (const { code } of i18n.locales) {
|
|
53
|
+
if (code === fallback) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const present = new Set(
|
|
57
|
+
graph.pages.flatMap((page) =>
|
|
58
|
+
page.locale === code ? [page.translationKey] : []
|
|
59
|
+
)
|
|
60
|
+
);
|
|
61
|
+
for (const [key, source] of fallbackPages) {
|
|
62
|
+
if (present.has(key)) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
routes.push({
|
|
66
|
+
alternates: alternatesByKey.get(key) ?? [],
|
|
67
|
+
collection: source.collection ?? "docs",
|
|
68
|
+
contentType: source.contentType,
|
|
69
|
+
draft: source.meta.draft,
|
|
70
|
+
editUrl: source.editUrl,
|
|
71
|
+
entryId: source.entryId ?? source.source.ref,
|
|
72
|
+
fallback: true,
|
|
73
|
+
hidden: source.meta.sidebar.hidden,
|
|
74
|
+
id: source.id,
|
|
75
|
+
indexable: false,
|
|
76
|
+
lastModified: source.lastModified,
|
|
77
|
+
locale: code,
|
|
78
|
+
path: withBasePath(basePath, localizeRoute(key, code, i18n)),
|
|
79
|
+
source: source.source,
|
|
80
|
+
sourcePath: source.sourcePath,
|
|
81
|
+
title: source.title,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return routes;
|
|
86
|
+
};
|
|
87
|
+
|
|
29
88
|
/** Build the runtime manifest that bridges core and the generated Astro app. */
|
|
30
89
|
export const buildManifest = (options: {
|
|
31
90
|
context: ProjectContext;
|
|
@@ -65,52 +124,10 @@ export const buildManifest = (options: {
|
|
|
65
124
|
title: page.title,
|
|
66
125
|
}));
|
|
67
126
|
|
|
68
|
-
// Fallback materialization: render the fallback locale's content at the
|
|
69
|
-
// localized URL for any translation a non-default locale is missing, so static
|
|
70
|
-
// output is fully prerendered (render-fallback, no client redirect). Fallback
|
|
71
|
-
// routes are not indexed and carry no `hreflang` of their own.
|
|
72
127
|
if (i18n) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
graph.pages
|
|
77
|
-
.filter((page) => page.locale === fallback)
|
|
78
|
-
.map((page) => [page.translationKey, page] as const)
|
|
79
|
-
);
|
|
80
|
-
for (const { code } of i18n.locales) {
|
|
81
|
-
if (code === fallback) {
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
const present = new Set(
|
|
85
|
-
graph.pages
|
|
86
|
-
.filter((page) => page.locale === code)
|
|
87
|
-
.map((page) => page.translationKey)
|
|
88
|
-
);
|
|
89
|
-
for (const [key, source] of fallbackPages) {
|
|
90
|
-
if (present.has(key)) {
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
routes.push({
|
|
94
|
-
alternates: alternatesByKey.get(key) ?? [],
|
|
95
|
-
collection: source.collection ?? "docs",
|
|
96
|
-
contentType: source.contentType,
|
|
97
|
-
draft: source.meta.draft,
|
|
98
|
-
editUrl: source.editUrl,
|
|
99
|
-
entryId: source.entryId ?? source.source.ref,
|
|
100
|
-
fallback: true,
|
|
101
|
-
hidden: source.meta.sidebar.hidden,
|
|
102
|
-
id: source.id,
|
|
103
|
-
indexable: false,
|
|
104
|
-
lastModified: source.lastModified,
|
|
105
|
-
locale: code,
|
|
106
|
-
path: localizeRoute(key, code, i18n),
|
|
107
|
-
source: source.source,
|
|
108
|
-
sourcePath: source.sourcePath,
|
|
109
|
-
title: source.title,
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
128
|
+
routes.push(
|
|
129
|
+
...buildFallbackRoutes(graph, i18n, alternatesByKey, config.basePath)
|
|
130
|
+
);
|
|
114
131
|
}
|
|
115
132
|
|
|
116
133
|
routes.sort((a, b) => a.path.localeCompare(b.path));
|
|
@@ -157,7 +157,7 @@ const hiddenInSidebarDiagnostics = (
|
|
|
157
157
|
pages: PageRecord[]
|
|
158
158
|
): Diagnostic[] => {
|
|
159
159
|
const hidden = new Set(
|
|
160
|
-
pages.
|
|
160
|
+
pages.flatMap((page) => (page.meta.sidebar.hidden ? [page.id] : []))
|
|
161
161
|
);
|
|
162
162
|
if (hidden.size === 0) {
|
|
163
163
|
return [];
|
package/src/core/navigation.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { extname } from "pathe";
|
|
2
2
|
|
|
3
|
+
import { stripBasePath, withBasePath } from "./base-path.ts";
|
|
3
4
|
import type {
|
|
4
5
|
FolderMeta,
|
|
5
6
|
SidebarDisplay,
|
|
@@ -28,7 +29,7 @@ const humanize = (segment: string): string =>
|
|
|
28
29
|
|
|
29
30
|
const numericOrder = (segment: string): number => {
|
|
30
31
|
const value = segment.match(NUMERIC_PREFIX)?.groups?.order;
|
|
31
|
-
return value ?
|
|
32
|
+
return value ? Math.trunc(Number(value)) : Number.POSITIVE_INFINITY;
|
|
32
33
|
};
|
|
33
34
|
|
|
34
35
|
/** The nav key of a raw path segment: group label or numeric-stripped name. */
|
|
@@ -37,6 +38,14 @@ const segmentKey = (raw: string): string => {
|
|
|
37
38
|
return (group ?? raw).replace(NUMERIC_PREFIX, "");
|
|
38
39
|
};
|
|
39
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Whether a filename stem is a directory index, ignoring an ordering prefix:
|
|
43
|
+
* route mapping strips the prefix before dropping `index`, so `01-index` routes
|
|
44
|
+
* exactly like `index` and must be treated as one here too.
|
|
45
|
+
*/
|
|
46
|
+
const isIndexStem = (stem: string): boolean =>
|
|
47
|
+
stem.replace(NUMERIC_PREFIX, "") === "index";
|
|
48
|
+
|
|
40
49
|
interface MutablePage {
|
|
41
50
|
kind: "page";
|
|
42
51
|
key: string;
|
|
@@ -105,7 +114,7 @@ const pageOrder = (page: PageRecord, filename: string): number => {
|
|
|
105
114
|
if (page.meta.sidebar.order !== undefined) {
|
|
106
115
|
return page.meta.sidebar.order;
|
|
107
116
|
}
|
|
108
|
-
if (filename.replace(extname(filename), "")
|
|
117
|
+
if (isIndexStem(filename.replace(extname(filename), ""))) {
|
|
109
118
|
return Number.NEGATIVE_INFINITY;
|
|
110
119
|
}
|
|
111
120
|
// Changelog entries read newest-first, matching the generated timeline. Sort
|
|
@@ -276,8 +285,9 @@ const buildFileSystemSidebar = (
|
|
|
276
285
|
// An index page's route IS its folder's route (no page segment to drop),
|
|
277
286
|
// and `(group)` folders contribute no route segment at all.
|
|
278
287
|
const routeSegments = page.route.split("/").filter(Boolean);
|
|
279
|
-
const folderParts =
|
|
280
|
-
|
|
288
|
+
const folderParts = isIndexStem(stem)
|
|
289
|
+
? routeSegments
|
|
290
|
+
: routeSegments.slice(0, -1);
|
|
281
291
|
const routeDirCount = dirs.filter((dir) => !GROUP_FOLDER.test(dir)).length;
|
|
282
292
|
const offset = Math.max(0, folderParts.length - routeDirCount);
|
|
283
293
|
|
|
@@ -326,82 +336,100 @@ const normalizeRef = (ref: string): string => {
|
|
|
326
336
|
|
|
327
337
|
const routeForRef = (
|
|
328
338
|
ref: string | undefined,
|
|
329
|
-
byRoute: Map<string, PageRecord
|
|
339
|
+
byRoute: Map<string, PageRecord>,
|
|
340
|
+
basePath: string
|
|
330
341
|
): string | undefined => {
|
|
331
342
|
if (!ref) {
|
|
332
343
|
return undefined;
|
|
333
344
|
}
|
|
334
345
|
const normalized = normalizeRef(ref);
|
|
335
|
-
|
|
346
|
+
// A matched page carries an already-based `route`; an unmatched ref is an
|
|
347
|
+
// author-written root-relative path that still needs the base applied.
|
|
348
|
+
return byRoute.get(normalized)?.route ?? withBasePath(basePath, normalized);
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Convert one non-group explicit-config sidebar item (string ref, `root`, or
|
|
353
|
+
* `href`) to a nav node, or null to skip. Group items (`item.items`) are handled
|
|
354
|
+
* by `buildConfigSidebar` itself so it owns the recursion.
|
|
355
|
+
*/
|
|
356
|
+
const configItemToNode = (
|
|
357
|
+
item: SidebarItemConfig,
|
|
358
|
+
byRoute: Map<string, PageRecord>,
|
|
359
|
+
basePath: string
|
|
360
|
+
): NavNode | null => {
|
|
361
|
+
if (typeof item === "string") {
|
|
362
|
+
const page = byRoute.get(normalizeRef(item));
|
|
363
|
+
if (!page) {
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
badge: page.meta.sidebar.badge,
|
|
368
|
+
deprecated: page.meta.deprecated || undefined,
|
|
369
|
+
description: page.description,
|
|
370
|
+
icon: page.meta.sidebar.icon,
|
|
371
|
+
kind: "page",
|
|
372
|
+
label: page.meta.sidebar.label ?? page.title,
|
|
373
|
+
pageId: page.id,
|
|
374
|
+
route: page.route,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (item.root) {
|
|
379
|
+
const page = byRoute.get(normalizeRef(item.root));
|
|
380
|
+
return {
|
|
381
|
+
badge: item.badge,
|
|
382
|
+
deprecated: page?.meta.deprecated || undefined,
|
|
383
|
+
icon: item.icon,
|
|
384
|
+
kind: "page",
|
|
385
|
+
label: item.label,
|
|
386
|
+
pageId: page?.id ?? "",
|
|
387
|
+
route: page?.route ?? withBasePath(basePath, normalizeRef(item.root)),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (item.href) {
|
|
392
|
+
return {
|
|
393
|
+
badge: item.badge,
|
|
394
|
+
icon: item.icon,
|
|
395
|
+
kind: "page",
|
|
396
|
+
label: item.label,
|
|
397
|
+
pageId: "",
|
|
398
|
+
route: withBasePath(basePath, item.href),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return null;
|
|
336
403
|
};
|
|
337
404
|
|
|
338
405
|
/** Build the sidebar tree from an explicit config spec. */
|
|
339
406
|
const buildConfigSidebar = (
|
|
340
407
|
items: SidebarItemConfig[],
|
|
341
408
|
byRoute: Map<string, PageRecord>,
|
|
342
|
-
display: SidebarDisplay
|
|
409
|
+
display: SidebarDisplay,
|
|
410
|
+
basePath: string
|
|
343
411
|
): NavNode[] => {
|
|
344
412
|
const nodes: NavNode[] = [];
|
|
345
|
-
|
|
346
413
|
for (const item of items) {
|
|
347
|
-
if (typeof item
|
|
348
|
-
const page = byRoute.get(normalizeRef(item));
|
|
349
|
-
if (page) {
|
|
350
|
-
nodes.push({
|
|
351
|
-
badge: page.meta.sidebar.badge,
|
|
352
|
-
deprecated: page.meta.deprecated || undefined,
|
|
353
|
-
description: page.description,
|
|
354
|
-
icon: page.meta.sidebar.icon,
|
|
355
|
-
kind: "page",
|
|
356
|
-
label: page.meta.sidebar.label ?? page.title,
|
|
357
|
-
pageId: page.id,
|
|
358
|
-
route: page.route,
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
continue;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
if (item.items) {
|
|
414
|
+
if (typeof item !== "string" && item.items) {
|
|
365
415
|
nodes.push({
|
|
366
416
|
badge: item.badge,
|
|
367
|
-
children: buildConfigSidebar(item.items, byRoute, display),
|
|
417
|
+
children: buildConfigSidebar(item.items, byRoute, display, basePath),
|
|
368
418
|
collapsed: item.collapsed,
|
|
369
419
|
directory: item.directory,
|
|
370
420
|
display: item.display ?? display,
|
|
371
421
|
icon: item.icon,
|
|
372
422
|
kind: "group",
|
|
373
423
|
label: item.label,
|
|
374
|
-
route: routeForRef(item.root, byRoute),
|
|
375
|
-
});
|
|
376
|
-
continue;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
if (item.root) {
|
|
380
|
-
const page = byRoute.get(normalizeRef(item.root));
|
|
381
|
-
nodes.push({
|
|
382
|
-
badge: item.badge,
|
|
383
|
-
deprecated: page?.meta.deprecated || undefined,
|
|
384
|
-
icon: item.icon,
|
|
385
|
-
kind: "page",
|
|
386
|
-
label: item.label,
|
|
387
|
-
pageId: page?.id ?? "",
|
|
388
|
-
route: page?.route ?? normalizeRef(item.root),
|
|
424
|
+
route: routeForRef(item.root, byRoute, basePath),
|
|
389
425
|
});
|
|
390
426
|
continue;
|
|
391
427
|
}
|
|
392
|
-
|
|
393
|
-
if (
|
|
394
|
-
nodes.push(
|
|
395
|
-
badge: item.badge,
|
|
396
|
-
icon: item.icon,
|
|
397
|
-
kind: "page",
|
|
398
|
-
label: item.label,
|
|
399
|
-
pageId: "",
|
|
400
|
-
route: item.href,
|
|
401
|
-
});
|
|
428
|
+
const node = configItemToNode(item, byRoute, basePath);
|
|
429
|
+
if (node) {
|
|
430
|
+
nodes.push(node);
|
|
402
431
|
}
|
|
403
432
|
}
|
|
404
|
-
|
|
405
433
|
return nodes;
|
|
406
434
|
};
|
|
407
435
|
|
|
@@ -409,6 +437,8 @@ const buildConfigSidebar = (
|
|
|
409
437
|
export const buildNavigation = (
|
|
410
438
|
pages: PageRecord[],
|
|
411
439
|
options: {
|
|
440
|
+
/** Site-wide route mount point (`""` or `/seg`); applied to config paths. */
|
|
441
|
+
basePath?: string;
|
|
412
442
|
folderMeta: Map<string, FolderMeta>;
|
|
413
443
|
/** Global display mode for every sidebar group (default `flat`). */
|
|
414
444
|
display?: SidebarDisplay;
|
|
@@ -426,30 +456,84 @@ export const buildNavigation = (
|
|
|
426
456
|
refByLogical?: boolean;
|
|
427
457
|
/** Shared `meta.$.*` meta, keyed by locale-stripped dir path. */
|
|
428
458
|
sharedFolderMeta?: Map<string, FolderMeta>;
|
|
459
|
+
/**
|
|
460
|
+
* The tree's root route before `basePath` (`"/"`, or the locale prefix
|
|
461
|
+
* under i18n, e.g. `/fr` — tab paths arrive already localized). The tab
|
|
462
|
+
* pointing here spans the whole tree, so it is excluded from tab-section
|
|
463
|
+
* scoping.
|
|
464
|
+
*/
|
|
465
|
+
localizedRoot?: string;
|
|
429
466
|
}
|
|
430
467
|
): Navigation => {
|
|
431
|
-
const
|
|
432
|
-
const selectors = options.selectors ?? [];
|
|
433
|
-
const tabs = options.tabs ?? [];
|
|
468
|
+
const basePath = options.basePath ?? "";
|
|
434
469
|
const display = options.display ?? "flat";
|
|
435
470
|
const metaPrefix = options.metaPrefix ?? "";
|
|
436
471
|
const sharedFolderMeta = options.sharedFolderMeta ?? new Map();
|
|
472
|
+
|
|
473
|
+
// Config-provided nav paths are authored as if mounted at root, so the base
|
|
474
|
+
// is applied here (idempotently, and only to internal paths — external URLs
|
|
475
|
+
// pass through). Content-derived sidebar routes are already based via
|
|
476
|
+
// `page.route`. The based tab paths also feed tab-scoping below, so they must
|
|
477
|
+
// agree with the based content routes. With no base, this is a pure pass-
|
|
478
|
+
// through — the arrays keep their exact authored shape.
|
|
479
|
+
const rebasePath = <T extends { path: string }>(item: T): T => ({
|
|
480
|
+
...item,
|
|
481
|
+
path: withBasePath(basePath, item.path),
|
|
482
|
+
});
|
|
483
|
+
const featured = basePath
|
|
484
|
+
? (options.featured ?? []).map((link) => ({
|
|
485
|
+
...link,
|
|
486
|
+
href: withBasePath(basePath, link.href),
|
|
487
|
+
}))
|
|
488
|
+
: (options.featured ?? []);
|
|
489
|
+
const selectors = basePath
|
|
490
|
+
? (options.selectors ?? []).map((selector) => ({
|
|
491
|
+
...selector,
|
|
492
|
+
items: selector.items.map(rebasePath),
|
|
493
|
+
}))
|
|
494
|
+
: (options.selectors ?? []);
|
|
495
|
+
const tabs = basePath
|
|
496
|
+
? (options.tabs ?? []).map((tab) => ({
|
|
497
|
+
...tab,
|
|
498
|
+
items: tab.items?.map(rebasePath),
|
|
499
|
+
path: withBasePath(basePath, tab.path),
|
|
500
|
+
}))
|
|
501
|
+
: (options.tabs ?? []);
|
|
437
502
|
const byRoute = new Map(
|
|
438
503
|
pages.map((page) => [
|
|
439
504
|
options.refByLogical ? page.translationKey : page.route,
|
|
440
505
|
page,
|
|
441
506
|
])
|
|
442
507
|
);
|
|
508
|
+
// Explicit-sidebar refs (`"foo/index"`) are authored as if mounted at root,
|
|
509
|
+
// but `page.route` carries the base — alias each page under its base-less
|
|
510
|
+
// route so a bare ref still resolves. (The i18n `refByLogical` map is already
|
|
511
|
+
// keyed by the base-less `translationKey`, so it needs no alias.)
|
|
512
|
+
if (basePath && !options.refByLogical) {
|
|
513
|
+
for (const page of pages) {
|
|
514
|
+
const bare = stripBasePath(basePath, page.route);
|
|
515
|
+
if (!byRoute.has(bare)) {
|
|
516
|
+
byRoute.set(bare, page);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
443
520
|
|
|
444
521
|
if (options.sidebar) {
|
|
445
522
|
return {
|
|
446
523
|
featured,
|
|
447
524
|
selectors,
|
|
448
|
-
sidebar: buildConfigSidebar(options.sidebar, byRoute, display),
|
|
525
|
+
sidebar: buildConfigSidebar(options.sidebar, byRoute, display, basePath),
|
|
449
526
|
tabs,
|
|
450
527
|
};
|
|
451
528
|
}
|
|
452
529
|
|
|
530
|
+
// A tab pointing at the tree root spans the whole sidebar rather than one
|
|
531
|
+
// section, so it must not feed tab-section hoisting. `tabs` carries final
|
|
532
|
+
// paths (localized, then based), so the root is compared in the same space —
|
|
533
|
+
// a root-level `(group)` folder's routePath is exactly the based/localized
|
|
534
|
+
// prefix (`/docs`, `/fr`) and a bare `"/"` check would miss the match (or,
|
|
535
|
+
// under a base, falsely scope a group named like the prefix).
|
|
536
|
+
const rootTabPath = withBasePath(basePath, options.localizedRoot ?? "/");
|
|
453
537
|
return {
|
|
454
538
|
featured,
|
|
455
539
|
selectors,
|
|
@@ -459,7 +543,9 @@ export const buildNavigation = (
|
|
|
459
543
|
sharedFolderMeta,
|
|
460
544
|
metaPrefix,
|
|
461
545
|
display,
|
|
462
|
-
new Set(
|
|
546
|
+
new Set(
|
|
547
|
+
tabs.flatMap((tab) => (tab.path === rootTabPath ? [] : [tab.path]))
|
|
548
|
+
)
|
|
463
549
|
),
|
|
464
550
|
tabs,
|
|
465
551
|
};
|
package/src/core/package-json.ts
CHANGED
|
@@ -15,8 +15,22 @@ export const toPackageName = (raw: string): string =>
|
|
|
15
15
|
* dependency pinned to the installed version plus `dev`/`build`/`doctor`
|
|
16
16
|
* scripts, so `npm install && npm run dev` works immediately. Shared by
|
|
17
17
|
* `blume init` and the migrators, which scaffold one when a project has none.
|
|
18
|
+
* `extraDeps` adds source SDKs (e.g. `@notionhq/client`) beside `blume`.
|
|
18
19
|
*/
|
|
19
|
-
export const blumePackageJson = (
|
|
20
|
+
export const blumePackageJson = (
|
|
21
|
+
name: string,
|
|
22
|
+
extraDeps: Record<string, string> = {}
|
|
23
|
+
): string => {
|
|
24
|
+
const dependencies = Object.entries({
|
|
25
|
+
blume: `^${getBlumeVersion()}`,
|
|
26
|
+
...extraDeps,
|
|
27
|
+
})
|
|
28
|
+
.toSorted(([a], [b]) => (a < b ? -1 : 1))
|
|
29
|
+
.map(
|
|
30
|
+
([dep, range]) => ` ${JSON.stringify(dep)}: ${JSON.stringify(range)}`
|
|
31
|
+
)
|
|
32
|
+
.join(",\n");
|
|
33
|
+
return `{
|
|
20
34
|
"name": ${JSON.stringify(name)},
|
|
21
35
|
"private": true,
|
|
22
36
|
"type": "module",
|
|
@@ -26,7 +40,8 @@ export const blumePackageJson = (name: string): string => `{
|
|
|
26
40
|
"doctor": "blume doctor"
|
|
27
41
|
},
|
|
28
42
|
"dependencies": {
|
|
29
|
-
|
|
43
|
+
${dependencies}
|
|
30
44
|
}
|
|
31
45
|
}
|
|
32
46
|
`;
|
|
47
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { relative } from "pathe";
|
|
2
2
|
|
|
3
3
|
import { loadConfig } from "./config.ts";
|
|
4
|
+
import { applyDeploymentEnv } from "./deployment-env.ts";
|
|
4
5
|
import { buildContentGraph } from "./graph.ts";
|
|
5
6
|
import { i18nDiagnostics } from "./i18n.ts";
|
|
6
7
|
import {
|
|
@@ -108,7 +109,7 @@ const entryIdDiagnostics = (
|
|
|
108
109
|
message: `Content source "${page.source.name}" is rooted outside the docs collection base, so ${page.route} resolves entry id "${entryId}" but the collection would generate "${expected}" — the page would 404 at runtime.`,
|
|
109
110
|
severity: "error",
|
|
110
111
|
suggestion:
|
|
111
|
-
"
|
|
112
|
+
"Use a single filesystem source (the docs collection roots at it), or root every filesystem source at content.root and partition them with include globs — a root at a subdirectory of content.root still mismatches.",
|
|
112
113
|
});
|
|
113
114
|
}
|
|
114
115
|
}
|
|
@@ -139,7 +140,13 @@ export const scanProject = async (
|
|
|
139
140
|
const configResult = await loadConfig(root, {
|
|
140
141
|
devServerUrl: options.devServerUrl,
|
|
141
142
|
});
|
|
142
|
-
|
|
143
|
+
// Re-run platform detection after CLI overrides: `loadConfig` already ran it,
|
|
144
|
+
// but adapter inference keys off `deployment.output`, which `--output server`
|
|
145
|
+
// only sets here. Idempotent for already-resolved fields — `deployment.site`
|
|
146
|
+
// keeps loadConfig's explicit > platform env > devServerUrl precedence.
|
|
147
|
+
const config = applyDeploymentEnv(
|
|
148
|
+
applyConfigOverrides(configResult.config, options.overrides)
|
|
149
|
+
);
|
|
143
150
|
const context = resolveProjectContext(root, config, {
|
|
144
151
|
runtimeDir: options.runtimeDir,
|
|
145
152
|
});
|
|
@@ -158,12 +165,11 @@ export const scanProject = async (
|
|
|
158
165
|
// Folder meta is discovered per filesystem source, under each source's own
|
|
159
166
|
// root and keyed by its route prefix, so a prefixed/root-differing source's
|
|
160
167
|
// `meta.ts` still lines up with its (prefixed) sidebar group path.
|
|
161
|
-
const metaSources: FolderMetaSource[] = sources
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
prefix: source.prefix,
|
|
165
|
-
|
|
166
|
-
}));
|
|
168
|
+
const metaSources: FolderMetaSource[] = sources.flatMap((source) =>
|
|
169
|
+
source.staged || !source.contentRoot
|
|
170
|
+
? []
|
|
171
|
+
: [{ prefix: source.prefix, root: source.contentRoot }]
|
|
172
|
+
);
|
|
167
173
|
|
|
168
174
|
// Run every source's `load()` in parallel, then funnel each entry through the
|
|
169
175
|
// shared `normalizeEntry` so route mapping is identical regardless of origin.
|
|
@@ -172,9 +178,9 @@ export const scanProject = async (
|
|
|
172
178
|
// `discoverFolderMeta`).
|
|
173
179
|
const localeDirs =
|
|
174
180
|
config.i18n && config.i18n.parser === "dir"
|
|
175
|
-
? config.i18n.locales
|
|
176
|
-
.
|
|
177
|
-
|
|
181
|
+
? config.i18n.locales.flatMap((locale) =>
|
|
182
|
+
locale.code === config.i18n?.defaultLocale ? [] : [locale.code]
|
|
183
|
+
)
|
|
178
184
|
: undefined;
|
|
179
185
|
|
|
180
186
|
const [loaded, folderMeta] = await Promise.all([
|
|
@@ -190,6 +196,7 @@ export const scanProject = async (
|
|
|
190
196
|
contentDiagnostics.push(...diagnostics);
|
|
191
197
|
for (const entry of entries) {
|
|
192
198
|
const normalized = normalizeEntry(entry, {
|
|
199
|
+
basePath: config.basePath,
|
|
193
200
|
defaultType: config.content.defaultType,
|
|
194
201
|
i18n: config.i18n,
|
|
195
202
|
source: {
|
|
@@ -217,11 +224,13 @@ export const scanProject = async (
|
|
|
217
224
|
const fsPaths = pages
|
|
218
225
|
.map((page) => page.sourcePath)
|
|
219
226
|
.filter((path): path is string => path !== undefined);
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
227
|
+
// The git pathspecs must cover where the pages actually live: each
|
|
228
|
+
// filesystem source's own root, which diverges from the global
|
|
229
|
+
// `content.root` when a source configures a non-default `root`.
|
|
230
|
+
const contentRoots = sources.flatMap((source) =>
|
|
231
|
+
source.staged || !source.contentRoot ? [] : [source.contentRoot]
|
|
224
232
|
);
|
|
233
|
+
const gitTimes = gitLastModifiedTimes(context.root, contentRoots, fsPaths);
|
|
225
234
|
for (const page of pages) {
|
|
226
235
|
if (!page.lastModified && page.sourcePath) {
|
|
227
236
|
page.lastModified = gitTimes.get(page.sourcePath);
|
|
@@ -230,6 +239,7 @@ export const scanProject = async (
|
|
|
230
239
|
}
|
|
231
240
|
|
|
232
241
|
const graph = buildContentGraph(pages, {
|
|
242
|
+
basePath: config.basePath,
|
|
233
243
|
folderMeta: folderMeta.meta,
|
|
234
244
|
i18n: config.i18n,
|
|
235
245
|
navigation: config.navigation,
|