blume 0.1.0 → 0.1.2
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/dist/cli/index.js +1213 -473
- package/dist/cli/index.js.map +33 -29
- package/dist/types/core/bridge.d.ts +24 -0
- package/dist/types/core/config.d.ts +35 -0
- package/dist/types/core/data.d.ts +129 -0
- package/dist/types/core/define-components.d.ts +27 -0
- package/dist/types/core/define-meta.d.ts +16 -0
- package/dist/types/core/deployment-env.d.ts +9 -0
- package/dist/types/core/diagnostics.d.ts +17 -0
- package/dist/types/core/i18n-ui.d.ts +500 -0
- package/dist/types/core/load-module.d.ts +7 -0
- package/dist/types/core/package-root.d.ts +17 -0
- package/dist/types/core/project.d.ts +9 -0
- package/dist/types/core/schema.d.ts +3461 -0
- package/dist/types/core/sources/types.d.ts +107 -0
- package/dist/types/core/types.d.ts +245 -0
- package/dist/types/core/ui-packs/ar.d.ts +3 -0
- package/dist/types/core/ui-packs/bg.d.ts +3 -0
- package/dist/types/core/ui-packs/bn.d.ts +3 -0
- package/dist/types/core/ui-packs/ca.d.ts +3 -0
- package/dist/types/core/ui-packs/cs.d.ts +3 -0
- package/dist/types/core/ui-packs/da.d.ts +3 -0
- package/dist/types/core/ui-packs/de.d.ts +3 -0
- package/dist/types/core/ui-packs/el.d.ts +3 -0
- package/dist/types/core/ui-packs/es.d.ts +3 -0
- package/dist/types/core/ui-packs/fa.d.ts +3 -0
- package/dist/types/core/ui-packs/fi.d.ts +3 -0
- package/dist/types/core/ui-packs/fr.d.ts +3 -0
- package/dist/types/core/ui-packs/he.d.ts +3 -0
- package/dist/types/core/ui-packs/hi.d.ts +3 -0
- package/dist/types/core/ui-packs/hr.d.ts +3 -0
- package/dist/types/core/ui-packs/hu.d.ts +3 -0
- package/dist/types/core/ui-packs/id.d.ts +3 -0
- package/dist/types/core/ui-packs/index.d.ts +13 -0
- package/dist/types/core/ui-packs/it.d.ts +3 -0
- package/dist/types/core/ui-packs/ja.d.ts +3 -0
- package/dist/types/core/ui-packs/ko.d.ts +3 -0
- package/dist/types/core/ui-packs/nl.d.ts +3 -0
- package/dist/types/core/ui-packs/no.d.ts +3 -0
- package/dist/types/core/ui-packs/pl.d.ts +3 -0
- package/dist/types/core/ui-packs/pt-br.d.ts +3 -0
- package/dist/types/core/ui-packs/pt.d.ts +3 -0
- package/dist/types/core/ui-packs/ro.d.ts +3 -0
- package/dist/types/core/ui-packs/ru.d.ts +3 -0
- package/dist/types/core/ui-packs/sk.d.ts +3 -0
- package/dist/types/core/ui-packs/sr.d.ts +3 -0
- package/dist/types/core/ui-packs/sv.d.ts +3 -0
- package/dist/types/core/ui-packs/th.d.ts +3 -0
- package/dist/types/core/ui-packs/tr.d.ts +3 -0
- package/dist/types/core/ui-packs/uk.d.ts +3 -0
- package/dist/types/core/ui-packs/vi.d.ts +3 -0
- package/dist/types/core/ui-packs/zh-tw.d.ts +3 -0
- package/dist/types/core/ui-packs/zh.d.ts +3 -0
- package/dist/types/core/version.d.ts +8 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/migrate/mintlify/config.d.ts +2 -0
- package/dist/types/migrate/mintlify/i18n.d.ts +7 -0
- package/dist/types/theme/fonts.d.ts +163 -0
- package/docs/advanced/custom-pages.mdx +91 -10
- package/docs/configuration/index.mdx +19 -0
- package/docs/content/components.mdx +103 -1
- package/package.json +13 -3
- package/src/ai/llms.ts +1 -2
- package/src/astro/examples.ts +106 -0
- package/src/astro/generate.ts +300 -75
- package/src/astro/index.ts +1 -1
- package/src/astro/islands.ts +1 -1
- package/src/astro/pages.ts +59 -0
- package/src/astro/templates.ts +180 -22
- package/src/cli/commands/validate.ts +1 -0
- package/src/components/content/CodeBlock.astro +28 -0
- package/src/components/content/Component.astro +65 -0
- package/src/components/content/Diff.astro +44 -0
- package/src/components/content/Tab.astro +8 -2
- package/src/components/content/diff.ts +95 -0
- package/src/components/layout/Favicon.astro +11 -4
- package/src/components/layout/PageLayout.astro +173 -0
- package/src/components/layout/ReferenceLayout.astro +6 -1
- package/src/components/layout/RootLayout.astro +9 -5
- package/src/components/layout/head-scripts.ts +19 -0
- package/src/core/data.ts +128 -0
- package/src/core/frontmatter.ts +43 -0
- package/src/core/links.ts +26 -10
- package/src/core/schema.ts +8 -0
- package/src/core/sources/filesystem.ts +1 -1
- package/src/core/sources/mdx-remote.ts +1 -2
- package/src/core/sources/mintlify.ts +1 -1
- package/src/core/sources/normalize.ts +19 -3
- package/src/core/sources/notion.ts +1 -1
- package/src/core/sources/sanity.ts +1 -1
- package/src/core/tsconfig-aliases.ts +201 -0
- package/src/index.ts +12 -0
- package/src/markdown/index.ts +69 -0
- package/src/migrate/fumadocs/config.ts +53 -4
- package/src/migrate/fumadocs/content.ts +1 -1
- package/src/migrate/fumadocs/groups.ts +230 -0
- package/src/migrate/fumadocs/index.ts +104 -19
- package/src/migrate/fumadocs/meta.ts +140 -10
- package/src/migrate/mintlify/snippets.ts +2 -1
- package/src/migrate/mintlify/transform.ts +1 -2
- package/src/migrate/nextra/index.ts +1 -1
- package/src/migrate/shared.ts +98 -1
- package/src/migrate/starlight/index.ts +1 -1
- package/src/registry/eject.ts +57 -10
- package/src/search/documents.ts +1 -2
- package/src/theme/entry.ts +29 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
---
|
|
2
|
+
// A full-width page layout for landing pages, marketing pages, dashboards — any
|
|
3
|
+
// page that wants Blume's chrome (document shell, header, theme, fonts) without
|
|
4
|
+
// the docs sidebar + prose + TOC grid that RootLayout hard-codes. It renders the
|
|
5
|
+
// `<head>`, theme/banner pre-paint scripts, banner, and header, then a single
|
|
6
|
+
// full-width `<slot />` for the body (plus an optional `footer` slot rendered
|
|
7
|
+
// after `<main>`). Import it into a custom page and pull props from `blume:data`:
|
|
8
|
+
//
|
|
9
|
+
// import PageLayout from "blume/components/layout/PageLayout.astro";
|
|
10
|
+
// import data from "blume:data";
|
|
11
|
+
// ---
|
|
12
|
+
// <PageLayout site={{ title: data.config.title }} logo={data.config.logo}
|
|
13
|
+
// navigation={data.navigation} themeMode={data.config.theme.mode}
|
|
14
|
+
// fontCssVars={data.fontCssVars} favicon={data.config.favicon}
|
|
15
|
+
// searchEnabled={data.config.search.enabled} page={{ title: "Home" }}>
|
|
16
|
+
// <!-- your full-width content -->
|
|
17
|
+
// </PageLayout>
|
|
18
|
+
import type {
|
|
19
|
+
BlumeBanner,
|
|
20
|
+
BlumeDataConfig,
|
|
21
|
+
BlumeFavicon,
|
|
22
|
+
BlumeLogo,
|
|
23
|
+
} from "../../core/data.ts";
|
|
24
|
+
import { EN_UI } from "../../core/i18n-ui.ts";
|
|
25
|
+
import type { UIStrings } from "../../core/i18n-ui.ts";
|
|
26
|
+
import type { LocaleSwitchOption, Navigation } from "../../core/types.ts";
|
|
27
|
+
import "blume:theme";
|
|
28
|
+
import Analytics from "./Analytics.astro";
|
|
29
|
+
import Banner from "./Banner.astro";
|
|
30
|
+
import Favicon from "./Favicon.astro";
|
|
31
|
+
import Fonts from "./Fonts.astro";
|
|
32
|
+
import { bannerInitScript, themeInitScript } from "./head-scripts.ts";
|
|
33
|
+
import Header from "./Header.astro";
|
|
34
|
+
|
|
35
|
+
interface Props {
|
|
36
|
+
site: { title: string; description?: string };
|
|
37
|
+
logo?: BlumeLogo | null;
|
|
38
|
+
favicon?: BlumeFavicon | null;
|
|
39
|
+
appleIcon?: BlumeFavicon | null;
|
|
40
|
+
banner?: BlumeBanner | null;
|
|
41
|
+
analytics?: BlumeDataConfig["analytics"];
|
|
42
|
+
navigation: Navigation;
|
|
43
|
+
/**
|
|
44
|
+
* Title/description/route for `<title>`, `og:*`, and the header active link.
|
|
45
|
+
* Unlike RootLayout, `title` is used verbatim (no `- siteTitle` suffix), since
|
|
46
|
+
* marketing pages usually set their own complete document title.
|
|
47
|
+
*/
|
|
48
|
+
page?: { title?: string; description?: string; route?: string };
|
|
49
|
+
themeMode: "system" | "light" | "dark";
|
|
50
|
+
fontCssVars?: string[];
|
|
51
|
+
searchEnabled: boolean;
|
|
52
|
+
askEnabled?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Absolute site URL (`data.config.site`). When set, `canonical` and the
|
|
55
|
+
* generated `og:image` for this route are derived from it automatically;
|
|
56
|
+
* `canonical`/`ogImage` below still override.
|
|
57
|
+
*/
|
|
58
|
+
siteUrl?: string | null;
|
|
59
|
+
/** Whether OG image generation is on (`data.config.og.enabled`). */
|
|
60
|
+
ogEnabled?: boolean;
|
|
61
|
+
/** SEO overrides; a marketing page often sets its own canonical/og image. */
|
|
62
|
+
ogImage?: string | null;
|
|
63
|
+
canonical?: string | null;
|
|
64
|
+
noindex?: boolean;
|
|
65
|
+
/** Active locale + direction for `<html lang>`/`<html dir>`. */
|
|
66
|
+
locale?: string;
|
|
67
|
+
dir?: "ltr" | "rtl";
|
|
68
|
+
/** Resolved UI dictionary; English baseline when omitted. */
|
|
69
|
+
ui?: UIStrings;
|
|
70
|
+
/** Language-switcher entries for the header. */
|
|
71
|
+
localeSwitch?: LocaleSwitchOption[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const {
|
|
75
|
+
site,
|
|
76
|
+
logo,
|
|
77
|
+
favicon,
|
|
78
|
+
appleIcon,
|
|
79
|
+
banner,
|
|
80
|
+
analytics,
|
|
81
|
+
navigation,
|
|
82
|
+
page,
|
|
83
|
+
themeMode,
|
|
84
|
+
fontCssVars,
|
|
85
|
+
searchEnabled,
|
|
86
|
+
askEnabled,
|
|
87
|
+
siteUrl,
|
|
88
|
+
ogEnabled,
|
|
89
|
+
ogImage,
|
|
90
|
+
canonical,
|
|
91
|
+
noindex,
|
|
92
|
+
locale = "en",
|
|
93
|
+
dir = "ltr",
|
|
94
|
+
ui,
|
|
95
|
+
localeSwitch,
|
|
96
|
+
} = Astro.props;
|
|
97
|
+
|
|
98
|
+
const strings = ui ?? EN_UI;
|
|
99
|
+
// Filter search to the active language only when the site is multi-locale.
|
|
100
|
+
const searchLocale =
|
|
101
|
+
localeSwitch && localeSwitch.length > 1 ? locale : undefined;
|
|
102
|
+
const pageTitle = page?.title ?? site.title;
|
|
103
|
+
const description = page?.description ?? site.description;
|
|
104
|
+
const route = page?.route ?? "/";
|
|
105
|
+
|
|
106
|
+
// Derive canonical + og:image from the site URL the same way the catch-all does
|
|
107
|
+
// for content pages, so a custom page gets both for free. The matching OG card
|
|
108
|
+
// (`/og/<slug>.png`, `index` for the home) is generated for static custom pages
|
|
109
|
+
// when `og.enabled`. Explicit props still win.
|
|
110
|
+
const ogSlug = route === "/" ? "index" : route.slice(1);
|
|
111
|
+
const resolvedCanonical =
|
|
112
|
+
canonical ?? (siteUrl ? `${siteUrl}${route === "/" ? "" : route}` : null);
|
|
113
|
+
const resolvedOgImage =
|
|
114
|
+
ogImage ?? (ogEnabled && siteUrl ? `${siteUrl}/og/${ogSlug}.png` : null);
|
|
115
|
+
|
|
116
|
+
const initialThemeScript = themeInitScript(themeMode);
|
|
117
|
+
const bannerScript = banner?.dismissible
|
|
118
|
+
? bannerInitScript(banner.key)
|
|
119
|
+
: null;
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
<!doctype html>
|
|
123
|
+
<html dir={dir} lang={locale}>
|
|
124
|
+
<head>
|
|
125
|
+
<meta charset="utf-8" />
|
|
126
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
127
|
+
<title>{pageTitle}</title>
|
|
128
|
+
<Favicon appleIcon={appleIcon} favicon={favicon} />
|
|
129
|
+
<Fonts cssVars={fontCssVars ?? []} />
|
|
130
|
+
{resolvedCanonical && <link href={resolvedCanonical} rel="canonical" />}
|
|
131
|
+
{noindex && <meta content="noindex" name="robots" />}
|
|
132
|
+
{description && <meta content={description} name="description" />}
|
|
133
|
+
<meta content="website" property="og:type" />
|
|
134
|
+
<meta content={pageTitle} property="og:title" />
|
|
135
|
+
{description && <meta content={description} property="og:description" />}
|
|
136
|
+
{
|
|
137
|
+
resolvedOgImage && (
|
|
138
|
+
<>
|
|
139
|
+
<meta content={resolvedOgImage} property="og:image" />
|
|
140
|
+
<meta content={resolvedOgImage} name="twitter:image" />
|
|
141
|
+
<meta content="summary_large_image" name="twitter:card" />
|
|
142
|
+
</>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
{bannerScript && <script is:inline set:html={bannerScript} />}
|
|
146
|
+
<script is:inline set:html={initialThemeScript} />
|
|
147
|
+
<Analytics analytics={analytics} />
|
|
148
|
+
</head>
|
|
149
|
+
<body class="bg-background font-sans text-foreground antialiased">
|
|
150
|
+
<a
|
|
151
|
+
class="absolute start-[-999px] top-0 z-[100] bg-accent px-4 py-2 text-accent-foreground focus:start-0"
|
|
152
|
+
href="#blume-content">{strings.page.skipToContent}</a
|
|
153
|
+
>
|
|
154
|
+
<Banner banner={banner} />
|
|
155
|
+
<Header
|
|
156
|
+
askEnabled={askEnabled}
|
|
157
|
+
hasSidebar={false}
|
|
158
|
+
localeSwitch={localeSwitch}
|
|
159
|
+
logo={logo}
|
|
160
|
+
navigation={navigation}
|
|
161
|
+
route={route}
|
|
162
|
+
searchEnabled={searchEnabled}
|
|
163
|
+
searchLocale={searchLocale}
|
|
164
|
+
searchStrings={strings.search}
|
|
165
|
+
site={site}
|
|
166
|
+
switcherStrings={strings.languageSwitcher}
|
|
167
|
+
>
|
|
168
|
+
<slot name="ask" slot="ask" />
|
|
169
|
+
</Header>
|
|
170
|
+
<main id="blume-content"><slot /></main>
|
|
171
|
+
<slot name="footer" />
|
|
172
|
+
</body>
|
|
173
|
+
</html>
|
|
@@ -23,6 +23,10 @@ interface Props {
|
|
|
23
23
|
href: string;
|
|
24
24
|
type?: string;
|
|
25
25
|
} | null;
|
|
26
|
+
appleIcon?: {
|
|
27
|
+
href: string;
|
|
28
|
+
type?: string;
|
|
29
|
+
} | null;
|
|
26
30
|
banner?: {
|
|
27
31
|
content: string;
|
|
28
32
|
link?: { text: string; href: string };
|
|
@@ -51,6 +55,7 @@ const {
|
|
|
51
55
|
site,
|
|
52
56
|
logo,
|
|
53
57
|
favicon,
|
|
58
|
+
appleIcon,
|
|
54
59
|
banner,
|
|
55
60
|
analytics,
|
|
56
61
|
navigation,
|
|
@@ -77,7 +82,7 @@ const bannerScript = banner?.dismissible
|
|
|
77
82
|
<meta charset="utf-8" />
|
|
78
83
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
79
84
|
<title>{pageTitle}</title>
|
|
80
|
-
<Favicon favicon={favicon} />
|
|
85
|
+
<Favicon favicon={favicon} appleIcon={appleIcon} />
|
|
81
86
|
<Fonts cssVars={fontCssVars ?? []} />
|
|
82
87
|
{bannerScript && <script is:inline set:html={bannerScript} />}
|
|
83
88
|
<script is:inline set:html={initialThemeScript} />
|
|
@@ -14,6 +14,7 @@ import Banner from "./Banner.astro";
|
|
|
14
14
|
import Breadcrumbs from "./Breadcrumbs.astro";
|
|
15
15
|
import Favicon from "./Favicon.astro";
|
|
16
16
|
import Fonts from "./Fonts.astro";
|
|
17
|
+
import { bannerInitScript, themeInitScript } from "./head-scripts.ts";
|
|
17
18
|
import Header from "./Header.astro";
|
|
18
19
|
import { findBreadcrumbs, flattenPages, getPagination } from "./nav-utils.ts";
|
|
19
20
|
import NavTree from "./NavTree.astro";
|
|
@@ -36,6 +37,10 @@ interface Props {
|
|
|
36
37
|
href: string;
|
|
37
38
|
type?: string;
|
|
38
39
|
} | null;
|
|
40
|
+
appleIcon?: {
|
|
41
|
+
href: string;
|
|
42
|
+
type?: string;
|
|
43
|
+
} | null;
|
|
39
44
|
banner?: {
|
|
40
45
|
content: string;
|
|
41
46
|
link?: { text: string; href: string };
|
|
@@ -107,6 +112,7 @@ const {
|
|
|
107
112
|
site,
|
|
108
113
|
logo,
|
|
109
114
|
favicon,
|
|
115
|
+
appleIcon,
|
|
110
116
|
banner,
|
|
111
117
|
analytics,
|
|
112
118
|
navigation,
|
|
@@ -201,11 +207,9 @@ const structuredDataJson = structuredData
|
|
|
201
207
|
? JSON.stringify(structuredData).replaceAll("<", "\\u003c")
|
|
202
208
|
: null;
|
|
203
209
|
|
|
204
|
-
const initialThemeScript =
|
|
205
|
-
|
|
206
|
-
// Pre-paint: hide a previously-dismissed banner before it flashes in.
|
|
210
|
+
const initialThemeScript = themeInitScript(themeMode);
|
|
207
211
|
const bannerScript = banner?.dismissible
|
|
208
|
-
?
|
|
212
|
+
? bannerInitScript(banner.key)
|
|
209
213
|
: null;
|
|
210
214
|
---
|
|
211
215
|
|
|
@@ -215,7 +219,7 @@ const bannerScript = banner?.dismissible
|
|
|
215
219
|
<meta charset="utf-8" />
|
|
216
220
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
217
221
|
<title>{pageTitle}</title>
|
|
218
|
-
<Favicon favicon={favicon} />
|
|
222
|
+
<Favicon favicon={favicon} appleIcon={appleIcon} />
|
|
219
223
|
<Fonts cssVars={fontCssVars ?? []} />
|
|
220
224
|
{canonical && <link rel="canonical" href={canonical} />}
|
|
221
225
|
{
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-paint inline scripts shared by the document layouts (`RootLayout`,
|
|
3
|
+
* `SplashLayout`). They run synchronously in `<head>`, before first paint, so
|
|
4
|
+
* the page never flashes the wrong theme or a since-dismissed banner. Kept in
|
|
5
|
+
* one place so the two layouts can't drift on this timing-critical logic.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Set `data-theme` from the stored preference (or the configured default, or the
|
|
10
|
+
* OS setting for `"system"`) before the body paints, avoiding a theme flash.
|
|
11
|
+
*/
|
|
12
|
+
export const themeInitScript = (
|
|
13
|
+
themeMode: "system" | "light" | "dark"
|
|
14
|
+
): string =>
|
|
15
|
+
`(()=>{const m=${JSON.stringify(themeMode)};const s=localStorage.getItem("blume-theme");const sys=matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light";document.documentElement.dataset.theme=s??(m==="system"?sys:m);})();`;
|
|
16
|
+
|
|
17
|
+
/** Hide a previously-dismissed banner before it can flash in. */
|
|
18
|
+
export const bannerInitScript = (key: string): string =>
|
|
19
|
+
`(()=>{if(localStorage.getItem("blume-banner:"+${JSON.stringify(key)}))document.documentElement.setAttribute("data-blume-banner-hidden","");})();`;
|
package/src/core/data.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { UIStrings } from "./i18n-ui.ts";
|
|
2
|
+
import type { ResolvedConfig, SearchProvider } from "./schema.ts";
|
|
3
|
+
import type { Navigation, RouteAlternate } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The shape of the `blume:data` virtual module — the resolved, serializable
|
|
7
|
+
* snapshot of a site that custom `.astro` pages read with
|
|
8
|
+
* `import data from "blume:data"`. `buildRuntimeData` (`astro/generate.ts`)
|
|
9
|
+
* produces exactly this object and annotates it with {@link BlumeData}, so the
|
|
10
|
+
* documented type and the emitted JSON stay in lockstep.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Resolved site logo: an inlined SVG, or light/dark image URLs. */
|
|
14
|
+
export interface BlumeLogo {
|
|
15
|
+
svg?: string;
|
|
16
|
+
light?: string;
|
|
17
|
+
dark?: string;
|
|
18
|
+
alt: string;
|
|
19
|
+
href: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A favicon or apple-touch-icon: a link href plus an optional MIME type. */
|
|
23
|
+
export interface BlumeFavicon {
|
|
24
|
+
href: string;
|
|
25
|
+
type?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Announcement banner, normalized from its config (string shorthand or object). */
|
|
29
|
+
export interface BlumeBanner {
|
|
30
|
+
content: string;
|
|
31
|
+
link?: { href: string; text: string };
|
|
32
|
+
dismissible: boolean;
|
|
33
|
+
/** Dismissal key: the configured id, else the content itself. */
|
|
34
|
+
key: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A generated syndication feed surfaced in the UI. */
|
|
38
|
+
export interface BlumeFeed {
|
|
39
|
+
href: string;
|
|
40
|
+
title: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** One configured locale, as exposed to the runtime. */
|
|
44
|
+
export interface BlumeDataLocale {
|
|
45
|
+
code: string;
|
|
46
|
+
dir: "ltr" | "rtl";
|
|
47
|
+
label: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Resolved i18n settings; `null` when the site is single-locale. */
|
|
51
|
+
export interface BlumeDataI18n {
|
|
52
|
+
defaultLocale: string;
|
|
53
|
+
/** Locale whose content renders for a missing translation; `null` disables it. */
|
|
54
|
+
fallbackLocale: string | null;
|
|
55
|
+
hideDefaultLocalePrefix: boolean;
|
|
56
|
+
locales: BlumeDataLocale[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** A single content route, with the metadata custom pages can read. */
|
|
60
|
+
export interface BlumeRoute {
|
|
61
|
+
/** Locales this logical page is translated into (excludes fallbacks). */
|
|
62
|
+
alternates: RouteAlternate[];
|
|
63
|
+
/** Astro collection the entry renders through (`"docs"` | `"staged"`). */
|
|
64
|
+
collection: string;
|
|
65
|
+
draft: boolean;
|
|
66
|
+
/** "Edit this page" URL, or `null` when no repo/source provides one. */
|
|
67
|
+
editUrl: string | null;
|
|
68
|
+
/** Astro collection entry id (for `getEntry`/`getCollection`); matches `id`. */
|
|
69
|
+
entryId: string;
|
|
70
|
+
/** True when this route renders fallback content for a missing translation. */
|
|
71
|
+
fallback: boolean;
|
|
72
|
+
hidden: boolean;
|
|
73
|
+
id: string;
|
|
74
|
+
/** Whether the page is part of the search index. */
|
|
75
|
+
indexable: boolean;
|
|
76
|
+
/** ISO "last updated" date when the feature is on, else `null`. */
|
|
77
|
+
lastModified: string | null;
|
|
78
|
+
/** Resolved locale code (the default locale when not under i18n). */
|
|
79
|
+
locale: string;
|
|
80
|
+
path: string;
|
|
81
|
+
title: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Site-wide settings derived from `blume.config` — the `config` field of {@link BlumeData}. */
|
|
85
|
+
export interface BlumeDataConfig {
|
|
86
|
+
analytics: NonNullable<ResolvedConfig["analytics"]> | null;
|
|
87
|
+
/** Apple touch icon, or `null` when none is configured/detected. */
|
|
88
|
+
appleIcon: BlumeFavicon | null;
|
|
89
|
+
banner: BlumeBanner | null;
|
|
90
|
+
/** `markdown.code.wrap`: wrap long code lines instead of scrolling. */
|
|
91
|
+
codeWrap: boolean;
|
|
92
|
+
description: string | undefined;
|
|
93
|
+
favicon: BlumeFavicon;
|
|
94
|
+
feedback: boolean;
|
|
95
|
+
i18n: BlumeDataI18n | null;
|
|
96
|
+
/** `markdown.imageZoom`: click-to-zoom content images. */
|
|
97
|
+
imageZoom: boolean;
|
|
98
|
+
logo: BlumeLogo | null;
|
|
99
|
+
/** Hosted MCP server, or `null` when MCP is off. */
|
|
100
|
+
mcp: { name: string; route: string } | null;
|
|
101
|
+
/** Open Graph image generation. */
|
|
102
|
+
og: { enabled: boolean };
|
|
103
|
+
/** Repository URL for header/edit links, or `null`. */
|
|
104
|
+
repoUrl: string | null;
|
|
105
|
+
search: { enabled: boolean; provider: SearchProvider };
|
|
106
|
+
/** Deployment site URL, or `null` when none is configured/detected. */
|
|
107
|
+
site: string | null;
|
|
108
|
+
structuredData: boolean;
|
|
109
|
+
theme: ResolvedConfig["theme"];
|
|
110
|
+
title: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The `blume:data` module a Blume site's custom pages import. */
|
|
114
|
+
export interface BlumeData {
|
|
115
|
+
config: BlumeDataConfig;
|
|
116
|
+
feeds: BlumeFeed[];
|
|
117
|
+
/** CSS variable names for the configured fonts (Astro `<Font>` integration). */
|
|
118
|
+
fontCssVars: string[];
|
|
119
|
+
/** Sidebar + tab tree for the default locale. */
|
|
120
|
+
navigation: Navigation;
|
|
121
|
+
/** Per-locale navigation trees, keyed by locale code (empty without i18n). */
|
|
122
|
+
navigationByLocale: Record<string, Navigation>;
|
|
123
|
+
routes: BlumeRoute[];
|
|
124
|
+
/** Resolved UI strings for the default locale. */
|
|
125
|
+
ui: UIStrings;
|
|
126
|
+
/** Per-locale UI strings, keyed by locale code (empty without i18n). */
|
|
127
|
+
uiByLocale: Record<string, UIStrings>;
|
|
128
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import baseMatter from "gray-matter";
|
|
2
|
+
import { dump, load } from "js-yaml";
|
|
3
|
+
|
|
4
|
+
// gray-matter@4 binds js-yaml 3's `safeLoad`/`safeDump` as its default YAML
|
|
5
|
+
// engine. In a workspace that pins js-yaml to v4 — where those functions were
|
|
6
|
+
// removed — parsing front matter throws "Function yaml.safeLoad is removed in
|
|
7
|
+
// js-yaml 4." Every Blume front-matter call routes through this `matter`
|
|
8
|
+
// wrapper, which supplies an explicit engine built on `load`/`dump`. Both exist
|
|
9
|
+
// in js-yaml 3 and 4, so Blume is immune to whichever version the consumer's
|
|
10
|
+
// install resolves for gray-matter.
|
|
11
|
+
|
|
12
|
+
type MatterInput = Parameters<typeof baseMatter>[0];
|
|
13
|
+
type MatterOptions = Parameters<typeof baseMatter>[1];
|
|
14
|
+
type StringifyArgs = Parameters<typeof baseMatter.stringify>;
|
|
15
|
+
|
|
16
|
+
const yamlEngine = {
|
|
17
|
+
parse: (input: string): object => (load(input) ?? {}) as object,
|
|
18
|
+
stringify: (data: object): string => dump(data),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const withYamlEngine = <O>(options: O): O =>
|
|
22
|
+
({
|
|
23
|
+
...options,
|
|
24
|
+
engines: {
|
|
25
|
+
yaml: yamlEngine,
|
|
26
|
+
...(options as { engines?: object })?.engines,
|
|
27
|
+
},
|
|
28
|
+
}) as O;
|
|
29
|
+
|
|
30
|
+
const matter = Object.assign(
|
|
31
|
+
(input: MatterInput, options?: MatterOptions) =>
|
|
32
|
+
baseMatter(input, withYamlEngine(options)),
|
|
33
|
+
baseMatter,
|
|
34
|
+
{
|
|
35
|
+
stringify: (
|
|
36
|
+
file: StringifyArgs[0],
|
|
37
|
+
data: StringifyArgs[1],
|
|
38
|
+
options?: StringifyArgs[2]
|
|
39
|
+
): string => baseMatter.stringify(file, data, withYamlEngine(options)),
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
export default matter;
|
package/src/core/links.ts
CHANGED
|
@@ -38,6 +38,8 @@ interface ExternalRef extends LinkSite {
|
|
|
38
38
|
interface LinkContext {
|
|
39
39
|
anchors: Map<string, Set<string>>;
|
|
40
40
|
publicDir: string | null;
|
|
41
|
+
/** Normalized `redirect.from` paths — valid targets that resolve at runtime. */
|
|
42
|
+
redirects: Set<string>;
|
|
41
43
|
routes: Set<string>;
|
|
42
44
|
}
|
|
43
45
|
|
|
@@ -129,16 +131,22 @@ const checkPathLink = (
|
|
|
129
131
|
}
|
|
130
132
|
|
|
131
133
|
const route = toRoute(resolved);
|
|
132
|
-
if (
|
|
133
|
-
return
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
134
|
+
if (ctx.routes.has(route)) {
|
|
135
|
+
return fragment ? checkAnchor(route, fragment, site, ctx) : null;
|
|
136
|
+
}
|
|
137
|
+
// A configured `redirect.from` resolves at runtime, so it's a valid target.
|
|
138
|
+
// Its destination (and any anchor there) is validated on its own page, so we
|
|
139
|
+
// don't follow the redirect to check the fragment here.
|
|
140
|
+
if (ctx.redirects.has(route)) {
|
|
141
|
+
return null;
|
|
140
142
|
}
|
|
141
|
-
return
|
|
143
|
+
return {
|
|
144
|
+
...site,
|
|
145
|
+
code: "BLUME_BROKEN_LINK",
|
|
146
|
+
message: `Broken link to ${target}: no page resolves to ${route}.`,
|
|
147
|
+
severity: "error",
|
|
148
|
+
suggestion: "Check the path, or create the target page.",
|
|
149
|
+
};
|
|
142
150
|
};
|
|
143
151
|
|
|
144
152
|
/** Probe a URL with the given method, normalizing failures to a result. */
|
|
@@ -298,11 +306,19 @@ const classifyLink = (
|
|
|
298
306
|
*/
|
|
299
307
|
export const validateLinks = async (
|
|
300
308
|
graph: ContentGraph,
|
|
301
|
-
options: {
|
|
309
|
+
options: {
|
|
310
|
+
publicDir: string | null;
|
|
311
|
+
checkExternal?: boolean;
|
|
312
|
+
/** Configured redirects; their `from` paths count as valid link targets. */
|
|
313
|
+
redirects?: { from: string }[];
|
|
314
|
+
}
|
|
302
315
|
): Promise<Diagnostic[]> => {
|
|
303
316
|
const ctx: LinkContext = {
|
|
304
317
|
anchors: buildAnchorIndex(graph.pages),
|
|
305
318
|
publicDir: options.publicDir,
|
|
319
|
+
redirects: new Set(
|
|
320
|
+
(options.redirects ?? []).map((redirect) => toRoute(redirect.from))
|
|
321
|
+
),
|
|
306
322
|
routes: new Set(graph.routes.keys()),
|
|
307
323
|
};
|
|
308
324
|
const diagnostics: Diagnostic[] = [];
|
package/src/core/schema.ts
CHANGED
|
@@ -1021,6 +1021,14 @@ export const blumeConfigSchema = z
|
|
|
1021
1021
|
contextual: contextualConfigSchema.default({}),
|
|
1022
1022
|
deployment: deploymentConfigSchema.default({}),
|
|
1023
1023
|
description: z.string().optional(),
|
|
1024
|
+
/**
|
|
1025
|
+
* Directory (relative to the project root) that `<Component path>` resolves
|
|
1026
|
+
* live previews and their source against. Defaults to `examples`; point it
|
|
1027
|
+
* elsewhere when examples live outside a top-level `examples/` — e.g. a
|
|
1028
|
+
* registry layout like `registry/<pkg>`, where a `<Component path>` key is
|
|
1029
|
+
* then relative to that directory.
|
|
1030
|
+
*/
|
|
1031
|
+
examples: z.string().default("examples"),
|
|
1024
1032
|
export: exportConfigSchema.default(false),
|
|
1025
1033
|
favicon: faviconConfigSchema.optional(),
|
|
1026
1034
|
feedback: z.boolean().default(true),
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { existsSync, watch as fsWatch } from "node:fs";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
|
|
4
|
-
import matter from "gray-matter";
|
|
5
4
|
import { extname, isAbsolute, join, relative, resolve } from "pathe";
|
|
6
5
|
import { glob } from "tinyglobby";
|
|
7
6
|
|
|
8
7
|
import { BlumeError } from "../diagnostics.ts";
|
|
8
|
+
import matter from "../frontmatter.ts";
|
|
9
9
|
import type { ContentSource, SourceEntry, SourceLoadResult } from "./types.ts";
|
|
10
10
|
|
|
11
11
|
/** Options for the built-in filesystem source. */
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { existsSync, watch as fsWatch } from "node:fs";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
|
|
4
|
-
import matter from "gray-matter";
|
|
5
4
|
import { isAbsolute, join, relative, resolve } from "pathe";
|
|
6
5
|
import { glob } from "tinyglobby";
|
|
7
6
|
|
|
8
7
|
import { transformMintlifyContent } from "../../migrate/mintlify/transform.ts";
|
|
9
8
|
import { BlumeError } from "../diagnostics.ts";
|
|
9
|
+
import matter from "../frontmatter.ts";
|
|
10
10
|
import type { Diagnostic } from "../types.ts";
|
|
11
11
|
import type { ContentSource, SourceEntry, SourceLoadResult } from "./types.ts";
|
|
12
12
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import GithubSlugger from "github-slugger";
|
|
1
2
|
import { extname } from "pathe";
|
|
2
3
|
|
|
3
4
|
import { diagnosticsFromZod } from "../diagnostics.ts";
|
|
@@ -19,7 +20,12 @@ const stripNumericPrefix = (segment: string): string =>
|
|
|
19
20
|
const groupLabel = (segment: string): string | null =>
|
|
20
21
|
segment.match(GROUP_FOLDER)?.groups?.label ?? null;
|
|
21
22
|
|
|
22
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* Slugify a content/route slug (Sanity, Notion, frontmatter `slug`). Heading
|
|
25
|
+
* anchor ids are *not* slugged here — they use a `github-slugger` in
|
|
26
|
+
* {@link extractHeadings}, matching the renderer (see `markdown/heading-anchors`)
|
|
27
|
+
* so `blume validate` checks anchors against the exact rendered heading ids.
|
|
28
|
+
*/
|
|
23
29
|
export const slugify = (text: string): string =>
|
|
24
30
|
text
|
|
25
31
|
.toLowerCase()
|
|
@@ -70,9 +76,19 @@ const mapRoute = (
|
|
|
70
76
|
const CODE_FENCE = /^```/u;
|
|
71
77
|
const ATX_HEADING = /^(?<hashes>#{1,6})\s+(?<text>.+?)\s*#*$/u;
|
|
72
78
|
|
|
73
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* Extract ATX headings from a markdown body, skipping fenced code blocks. Each
|
|
81
|
+
* heading's anchor slug comes from a per-document `github-slugger` — the exact
|
|
82
|
+
* slugger the renderer uses (`markdown/heading-anchors`) — advanced over every
|
|
83
|
+
* `#`–`######` in document order. Matching it (rather than a hand-rolled
|
|
84
|
+
* slugify) keeps the manifest's anchor ids identical to the rendered ones, so
|
|
85
|
+
* `blume validate` stops false-flagging links like `#the-read--write-fallback`
|
|
86
|
+
* (a hand slugify collapses `--`; github-slugger keeps it) and resolves repeated
|
|
87
|
+
* headings the same way (`setup`, `setup-1`).
|
|
88
|
+
*/
|
|
74
89
|
export const extractHeadings = (body: string): Heading[] => {
|
|
75
90
|
const headings: Heading[] = [];
|
|
91
|
+
const slugger = new GithubSlugger();
|
|
76
92
|
let inFence = false;
|
|
77
93
|
|
|
78
94
|
for (const line of body.split("\n")) {
|
|
@@ -87,7 +103,7 @@ export const extractHeadings = (body: string): Heading[] => {
|
|
|
87
103
|
if (match?.groups) {
|
|
88
104
|
const depth = match.groups.hashes?.length ?? 1;
|
|
89
105
|
const text = (match.groups.text ?? "").trim();
|
|
90
|
-
headings.push({ depth, slug:
|
|
106
|
+
headings.push({ depth, slug: slugger.slug(text), text });
|
|
91
107
|
}
|
|
92
108
|
}
|
|
93
109
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import matter from "gray-matter";
|
|
2
1
|
import { join } from "pathe";
|
|
3
2
|
|
|
4
3
|
import { BlumeError } from "../diagnostics.ts";
|
|
4
|
+
import matter from "../frontmatter.ts";
|
|
5
5
|
import type { Diagnostic } from "../types.ts";
|
|
6
6
|
import { materializeAssets } from "./assets.ts";
|
|
7
7
|
import {
|