blume 0.2.0 → 0.4.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/dist/cli/index.js +2429 -792
- package/dist/cli/index.js.map +63 -44
- package/dist/types/core/data.d.ts +16 -0
- package/dist/types/core/define-components.d.ts +9 -2
- package/dist/types/core/diagnostics.d.ts +5 -0
- package/dist/types/core/schema.d.ts +313 -778
- package/dist/types/core/types.d.ts +2 -2
- package/dist/types/migrate/mintlify/assets.d.ts +8 -0
- package/docs/01-quickstart.mdx +5 -16
- package/docs/02-deployment.mdx +26 -40
- package/docs/advanced/api-reference.mdx +10 -37
- package/docs/advanced/blog.mdx +9 -25
- package/docs/advanced/changelog.mdx +10 -33
- package/docs/advanced/custom-pages.mdx +66 -61
- package/docs/configuration/ai.mdx +47 -91
- package/docs/configuration/analytics.mdx +20 -38
- package/docs/configuration/customization.mdx +92 -27
- package/docs/configuration/export.mdx +9 -34
- package/docs/configuration/index.mdx +78 -85
- package/docs/configuration/search.mdx +17 -54
- package/docs/configuration/seo.mdx +18 -44
- package/docs/configuration/theming.mdx +20 -42
- package/docs/content/components.mdx +42 -101
- package/docs/content/i18n.mdx +21 -72
- package/docs/content/index.mdx +18 -48
- package/docs/content/islands.mdx +79 -33
- package/docs/content/meta.mdx +23 -50
- package/docs/content/navigation.mdx +42 -56
- package/docs/content/sources.mdx +20 -83
- package/docs/content/syntax.mdx +37 -105
- package/docs/index.mdx +13 -51
- package/docs/reference/cli.mdx +49 -18
- package/docs/reference/frontmatter.mdx +2 -5
- package/package.json +3 -1
- package/src/ai/ask-context.ts +131 -0
- package/src/ai/ask-data.ts +25 -0
- package/src/astro/component-slots.ts +165 -0
- package/src/astro/generate.ts +132 -13
- package/src/astro/integration.ts +85 -3
- package/src/astro/islands.ts +6 -2
- package/src/astro/markdown-negotiation.ts +17 -3
- package/src/astro/pages.ts +11 -13
- package/src/astro/static-assets.ts +117 -0
- package/src/astro/templates.ts +120 -50
- package/src/blume-modules.d.ts +25 -0
- package/src/cli/args.ts +23 -0
- package/src/cli/commands/build.ts +209 -1
- package/src/cli/commands/check.ts +62 -0
- package/src/cli/commands/dev.ts +32 -3
- package/src/cli/commands/doctor.ts +32 -6
- package/src/cli/commands/eject.ts +3 -1
- package/src/cli/commands/init.ts +184 -16
- package/src/cli/commands/preview.ts +2 -1
- package/src/cli/commands/validate.ts +27 -2
- package/src/cli/dev-lock.ts +84 -0
- package/src/cli/index.ts +15 -0
- package/src/cli/internal-error.ts +63 -0
- package/src/cli/log.ts +41 -1
- package/src/cli/prepare.ts +17 -3
- package/src/cli/required-secrets.ts +44 -0
- package/src/components/BlumePage.astro +109 -0
- package/src/components/content/YouTube.astro +35 -0
- package/src/components/content/youtube.ts +46 -0
- package/src/components/index.ts +3 -3
- package/src/components/islands/ask-ai.tsx +29 -15
- package/src/components/islands/hooks.ts +188 -0
- package/src/components/layout/Empty.astro +6 -0
- package/src/components/layout/Header.astro +24 -39
- package/src/components/layout/Logo.astro +50 -0
- package/src/components/layout/NavSelector.astro +75 -0
- package/src/components/layout/PageLayout.astro +38 -2
- package/src/components/layout/RootLayout.astro +70 -4
- package/src/components/layout/hydration-hint.ts +30 -0
- package/src/components/layout/overrides.ts +6 -4
- package/src/components/props.ts +71 -0
- package/src/core/assets.ts +31 -0
- package/src/core/bridge.ts +10 -0
- package/src/core/builtin-tags.ts +40 -0
- package/src/core/component-diagnostics.ts +44 -0
- package/src/core/component-overrides.ts +478 -0
- package/src/core/config.ts +8 -0
- package/src/core/data.ts +14 -0
- package/src/core/define-components.ts +9 -2
- package/src/core/diagnostics.ts +95 -1
- package/src/core/gitignore.ts +30 -0
- package/src/core/graph.ts +7 -0
- package/src/core/links.ts +60 -19
- package/src/core/nav-diagnostics.ts +205 -0
- package/src/core/project-graph.ts +40 -1
- package/src/core/schema.ts +35 -96
- package/src/core/sources/mdx-remote.ts +54 -8
- package/src/core/sources/normalize.ts +57 -1
- package/src/core/sources/notion.ts +49 -5
- package/src/core/sources/sanity.ts +5 -1
- package/src/core/types.ts +2 -2
- package/src/deploy/redirects.ts +43 -0
- package/src/deploy/rss.ts +1 -8
- package/src/deploy/sitemap.ts +20 -1
- package/src/deploy/xml.ts +8 -0
- package/src/markdown/directives.ts +15 -7
- package/src/markdown/package-commands.ts +26 -4
- package/src/migrate/fumadocs/content.ts +14 -1
- package/src/migrate/fumadocs/groups.ts +7 -0
- package/src/migrate/fumadocs/index.ts +5 -2
- package/src/migrate/mintlify/assets.ts +46 -0
- package/src/migrate/mintlify/config.ts +1 -176
- package/src/migrate/mintlify/index.ts +53 -45
- package/src/migrate/shared.ts +12 -27
- package/src/migrate/starlight/config.ts +0 -4
- package/src/og/card.ts +175 -38
- package/src/registry/eject.ts +52 -12
- package/src/registry/registry.ts +172 -0
- package/src/registry/rewrite-imports.ts +31 -19
- package/src/runtime/index.ts +61 -0
- package/src/search/documents.ts +23 -5
- package/src/search/sync/algolia.ts +5 -1
- package/src/search/sync/typesense.ts +24 -16
- package/src/theme/palette.ts +26 -7
- package/src/vite-env.d.ts +14 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
---
|
|
2
|
+
// A no-op layout slot: renders nothing regardless of props. Used as the built-in
|
|
3
|
+
// fallback for the content-injection slots (Footer, PageHeader, PageFooter) that
|
|
4
|
+
// have no default, so an unconfigured slot renders empty while `resolveSlot`
|
|
5
|
+
// still returns a valid component and the generated pages stay type-safe.
|
|
6
|
+
---
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
---
|
|
2
|
+
import type { ComponentOverride } from "../../core/define-components.ts";
|
|
2
3
|
import type { UIStrings } from "../../core/i18n-ui.ts";
|
|
3
4
|
import type { LocaleSwitchOption, Navigation } from "../../core/types.ts";
|
|
4
5
|
import { GITHUB_MARK } from "../github-mark.ts";
|
|
5
6
|
import Icon from "../Icon.astro";
|
|
6
7
|
import LanguageSwitcher from "./LanguageSwitcher.astro";
|
|
8
|
+
import Logo from "./Logo.astro";
|
|
9
|
+
import NavSelector from "./NavSelector.astro";
|
|
10
|
+
import { resolveSlot } from "./overrides.ts";
|
|
7
11
|
import Search from "./Search.astro";
|
|
8
12
|
|
|
9
13
|
interface Props {
|
|
@@ -27,6 +31,12 @@ interface Props {
|
|
|
27
31
|
localeSwitch?: LocaleSwitchOption[];
|
|
28
32
|
/** Active locale for per-language search filtering. */
|
|
29
33
|
searchLocale?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Layout-slot overrides forwarded from the root layout. The header honors
|
|
36
|
+
* `Logo` and `Search` here so those pieces can be replaced without swapping
|
|
37
|
+
* the whole header.
|
|
38
|
+
*/
|
|
39
|
+
layout?: Record<string, ComponentOverride>;
|
|
30
40
|
}
|
|
31
41
|
|
|
32
42
|
const {
|
|
@@ -41,15 +51,11 @@ const {
|
|
|
41
51
|
switcherStrings,
|
|
42
52
|
localeSwitch,
|
|
43
53
|
searchLocale,
|
|
54
|
+
layout = {},
|
|
44
55
|
} = Astro.props;
|
|
45
56
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const logoSvg = logo?.svg;
|
|
49
|
-
const logoLight = logo?.light;
|
|
50
|
-
const logoDark = logo?.dark ?? logo?.light;
|
|
51
|
-
const logoAlt = logo?.alt ?? "";
|
|
52
|
-
const brandHref = logo?.href ?? "/";
|
|
57
|
+
const LogoSlot = resolveSlot(layout.Logo, Logo);
|
|
58
|
+
const SearchSlot = resolveSlot(layout.Search, Search);
|
|
53
59
|
|
|
54
60
|
const iconButton =
|
|
55
61
|
"inline-flex size-9 cursor-pointer items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground";
|
|
@@ -80,37 +86,16 @@ const clickScript = `document.addEventListener("click",(e)=>{const t=e.target.cl
|
|
|
80
86
|
</button>
|
|
81
87
|
)
|
|
82
88
|
}
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
/>
|
|
94
|
-
)
|
|
95
|
-
}
|
|
96
|
-
{
|
|
97
|
-
!logoSvg &&
|
|
98
|
-
logoLight &&
|
|
99
|
-
(logoLight === logoDark ? (
|
|
100
|
-
<img alt={logoAlt} class="h-5 w-auto" src={logoLight} />
|
|
101
|
-
) : (
|
|
102
|
-
<>
|
|
103
|
-
<img alt={logoAlt} class="h-5 w-auto dark:hidden" src={logoLight} />
|
|
104
|
-
<img
|
|
105
|
-
alt={logoAlt}
|
|
106
|
-
class="hidden h-5 w-auto dark:block"
|
|
107
|
-
src={logoDark}
|
|
108
|
-
/>
|
|
109
|
-
</>
|
|
110
|
-
))
|
|
111
|
-
}
|
|
112
|
-
{site.title}
|
|
113
|
-
</a>
|
|
89
|
+
<LogoSlot logo={logo} site={site} />
|
|
90
|
+
{
|
|
91
|
+
navigation.selectors.length > 0 && (
|
|
92
|
+
<div class="flex items-center gap-1.5">
|
|
93
|
+
{navigation.selectors.map((selector) => (
|
|
94
|
+
<NavSelector route={route} selector={selector} />
|
|
95
|
+
))}
|
|
96
|
+
</div>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
114
99
|
{
|
|
115
100
|
navigation.tabs.length > 0 && (
|
|
116
101
|
<nav aria-label="Sections" class="hidden gap-1 md:flex">
|
|
@@ -144,7 +129,7 @@ const clickScript = `document.addEventListener("click",(e)=>{const t=e.target.cl
|
|
|
144
129
|
}
|
|
145
130
|
{
|
|
146
131
|
searchEnabled && (
|
|
147
|
-
<
|
|
132
|
+
<SearchSlot
|
|
148
133
|
askEnabled={askEnabled}
|
|
149
134
|
locale={searchLocale}
|
|
150
135
|
navigation={navigation}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
// The site brand: the logo mark (inline SVG so a `currentColor` mark follows the
|
|
3
|
+
// theme, or light/dark `<img>` variants) followed by the site title, wrapped in
|
|
4
|
+
// a link to the brand href. Extracted from the header so it can be replaced on
|
|
5
|
+
// its own through the `Logo` layout slot (`defineComponents({ layout: { Logo } })`).
|
|
6
|
+
import type { BlumeLogo } from "../../core/data.ts";
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
site: { title: string };
|
|
10
|
+
logo?: BlumeLogo | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { site, logo } = Astro.props;
|
|
14
|
+
|
|
15
|
+
// The logo is resolved upstream: an inline `svg` (so a currentColor mark follows
|
|
16
|
+
// the theme) or image URL(s) for an <img>.
|
|
17
|
+
const logoSvg = logo?.svg;
|
|
18
|
+
const logoLight = logo?.light;
|
|
19
|
+
const logoDark = logo?.dark ?? logo?.light;
|
|
20
|
+
const logoAlt = logo?.alt ?? "";
|
|
21
|
+
const brandHref = logo?.href ?? "/";
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
<a
|
|
25
|
+
class="inline-flex items-center gap-2 font-semibold text-base text-foreground"
|
|
26
|
+
href={brandHref}
|
|
27
|
+
>
|
|
28
|
+
{
|
|
29
|
+
logoSvg && (
|
|
30
|
+
<span
|
|
31
|
+
aria-hidden="true"
|
|
32
|
+
class="inline-flex h-5 items-center [&>svg]:h-5 [&>svg]:w-auto"
|
|
33
|
+
set:html={logoSvg}
|
|
34
|
+
/>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
{
|
|
38
|
+
!logoSvg &&
|
|
39
|
+
logoLight &&
|
|
40
|
+
(logoLight === logoDark ? (
|
|
41
|
+
<img alt={logoAlt} class="h-5 w-auto" src={logoLight} />
|
|
42
|
+
) : (
|
|
43
|
+
<>
|
|
44
|
+
<img alt={logoAlt} class="h-5 w-auto dark:hidden" src={logoLight} />
|
|
45
|
+
<img alt={logoAlt} class="hidden h-5 w-auto dark:block" src={logoDark} />
|
|
46
|
+
</>
|
|
47
|
+
))
|
|
48
|
+
}
|
|
49
|
+
{site.title}
|
|
50
|
+
</a>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
// A top-level navigation selector (Mintlify-style): a dropdown that switches
|
|
3
|
+
// between partitions of the site — a product, a version, or any grouped set of
|
|
4
|
+
// destinations (`navigation.selectors` in the config). Zero-JS, built on
|
|
5
|
+
// <details>/<summary> like the language switcher.
|
|
6
|
+
import type { NavSelector } from "../../core/types.ts";
|
|
7
|
+
import Icon from "../Icon.astro";
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
selector: NavSelector;
|
|
11
|
+
route: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { selector, route } = Astro.props;
|
|
15
|
+
|
|
16
|
+
// The active item is the deepest path that prefixes the current route, falling
|
|
17
|
+
// back to the first item so the summary always shows something meaningful.
|
|
18
|
+
const active =
|
|
19
|
+
selector.items.find((item) => item.path === route) ??
|
|
20
|
+
selector.items
|
|
21
|
+
.filter((item) => route.startsWith(item.path))
|
|
22
|
+
.toSorted((a, b) => b.path.length - a.path.length)[0] ??
|
|
23
|
+
selector.items[0];
|
|
24
|
+
|
|
25
|
+
const iconButton =
|
|
26
|
+
"inline-flex h-9 cursor-pointer items-center gap-1.5 rounded-full border border-border bg-background px-3 text-muted-foreground text-sm transition-colors hover:border-foreground hover:text-foreground";
|
|
27
|
+
const menuRowClass =
|
|
28
|
+
"flex w-full items-start gap-2.5 rounded-md px-2 py-1.5 text-start text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground aria-[current=true]:text-foreground";
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
{
|
|
32
|
+
selector.items.length > 0 && (
|
|
33
|
+
<details class="group relative">
|
|
34
|
+
<summary
|
|
35
|
+
aria-label={selector.label}
|
|
36
|
+
class={`${iconButton} list-none [&::-webkit-details-marker]:hidden`}
|
|
37
|
+
>
|
|
38
|
+
{active?.icon && <Icon name={active.icon} size={16} />}
|
|
39
|
+
<span class="max-sm:hidden">{active?.label ?? selector.label}</span>
|
|
40
|
+
<Icon
|
|
41
|
+
class="transition-transform group-open:rotate-180"
|
|
42
|
+
name="chevron-down"
|
|
43
|
+
size={14}
|
|
44
|
+
/>
|
|
45
|
+
</summary>
|
|
46
|
+
<div class="absolute start-0 z-50 mt-2 min-w-56 rounded-blume border border-border bg-background p-1 shadow-xl">
|
|
47
|
+
{selector.items.map((item) => (
|
|
48
|
+
<a
|
|
49
|
+
aria-current={item.path === active?.path ? "true" : undefined}
|
|
50
|
+
class={menuRowClass}
|
|
51
|
+
href={item.path}
|
|
52
|
+
>
|
|
53
|
+
{item.icon && <Icon class="mt-0.5" name={item.icon} size={16} />}
|
|
54
|
+
<span class="flex-1">
|
|
55
|
+
<span class="flex items-center gap-2">
|
|
56
|
+
{item.label}
|
|
57
|
+
{item.tag && (
|
|
58
|
+
<span class="rounded-full bg-muted px-1.5 py-0.5 text-[0.65rem] text-muted-foreground">
|
|
59
|
+
{item.tag}
|
|
60
|
+
</span>
|
|
61
|
+
)}
|
|
62
|
+
</span>
|
|
63
|
+
{item.description && (
|
|
64
|
+
<span class="mt-0.5 block text-muted-foreground text-xs">
|
|
65
|
+
{item.description}
|
|
66
|
+
</span>
|
|
67
|
+
)}
|
|
68
|
+
</span>
|
|
69
|
+
{item.path === active?.path && <Icon name="check" size={14} />}
|
|
70
|
+
</a>
|
|
71
|
+
))}
|
|
72
|
+
</div>
|
|
73
|
+
</details>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
// </PageLayout>
|
|
18
18
|
import type {
|
|
19
19
|
BlumeBanner,
|
|
20
|
+
BlumeClientData,
|
|
20
21
|
BlumeDataConfig,
|
|
21
22
|
BlumeFavicon,
|
|
22
23
|
BlumeLogo,
|
|
@@ -69,6 +70,12 @@ interface Props {
|
|
|
69
70
|
ui?: UIStrings;
|
|
70
71
|
/** Language-switcher entries for the header. */
|
|
71
72
|
localeSwitch?: LocaleSwitchOption[];
|
|
73
|
+
/**
|
|
74
|
+
* Snapshot for React island hooks (`blume/hooks`). Pass it when a custom page
|
|
75
|
+
* hosts islands that read `useBlume()`/`usePage()`:
|
|
76
|
+
* `clientData={{ config: data.config, navigation: data.navigation, page: { route: "/", title } }}`.
|
|
77
|
+
*/
|
|
78
|
+
clientData?: BlumeClientData | null;
|
|
72
79
|
}
|
|
73
80
|
|
|
74
81
|
const {
|
|
@@ -93,8 +100,13 @@ const {
|
|
|
93
100
|
dir = "ltr",
|
|
94
101
|
ui,
|
|
95
102
|
localeSwitch,
|
|
103
|
+
clientData,
|
|
96
104
|
} = Astro.props;
|
|
97
105
|
|
|
106
|
+
const clientDataJson = clientData
|
|
107
|
+
? JSON.stringify(clientData).replaceAll("<", "\\u003c")
|
|
108
|
+
: null;
|
|
109
|
+
|
|
98
110
|
const strings = ui ?? EN_UI;
|
|
99
111
|
// Filter search to the active language only when the site is multi-locale.
|
|
100
112
|
const searchLocale =
|
|
@@ -110,8 +122,17 @@ const route = page?.route ?? "/";
|
|
|
110
122
|
const ogSlug = route === "/" ? "index" : route.slice(1);
|
|
111
123
|
const resolvedCanonical =
|
|
112
124
|
canonical ?? (siteUrl ? `${siteUrl}${route === "/" ? "" : route}` : null);
|
|
113
|
-
|
|
114
|
-
|
|
125
|
+
// An explicit `ogImage` wins. A root-relative path (e.g. an image dropped in
|
|
126
|
+
// `public/`) is resolved against the site URL so crawlers get an absolute
|
|
127
|
+
// `og:image`; an already-absolute URL passes through untouched. Otherwise fall
|
|
128
|
+
// back to the generated OG card for this route.
|
|
129
|
+
const absolutizeOgImage = (value: string): string =>
|
|
130
|
+
value.startsWith("/") && siteUrl ? `${siteUrl}${value}` : value;
|
|
131
|
+
const resolvedOgImage = ogImage
|
|
132
|
+
? absolutizeOgImage(ogImage)
|
|
133
|
+
: ogEnabled && siteUrl
|
|
134
|
+
? `${siteUrl}/og/${ogSlug}.png`
|
|
135
|
+
: null;
|
|
115
136
|
|
|
116
137
|
const initialThemeScript = themeInitScript(themeMode);
|
|
117
138
|
const bannerScript = banner?.dismissible
|
|
@@ -169,5 +190,20 @@ const bannerScript = banner?.dismissible
|
|
|
169
190
|
</Header>
|
|
170
191
|
<main id="blume-content"><slot /></main>
|
|
171
192
|
<slot name="footer" />
|
|
193
|
+
{
|
|
194
|
+
clientDataJson && (
|
|
195
|
+
<script
|
|
196
|
+
id="blume-client-data"
|
|
197
|
+
is:inline
|
|
198
|
+
set:html={clientDataJson}
|
|
199
|
+
type="application/json"
|
|
200
|
+
/>
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
<script>
|
|
204
|
+
// Dev-only: friendly hint after a React island hydration mismatch;
|
|
205
|
+
// tree-shaken out of production builds.
|
|
206
|
+
import "./hydration-hint.ts";
|
|
207
|
+
</script>
|
|
172
208
|
</body>
|
|
173
209
|
</html>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { EN_UI } from "../../core/i18n-ui.ts";
|
|
3
3
|
import type { UIStrings } from "../../core/i18n-ui.ts";
|
|
4
|
+
import type { BlumeClientData } from "../../core/data.ts";
|
|
4
5
|
import type {
|
|
5
6
|
Heading,
|
|
6
7
|
LocaleSwitchOption,
|
|
@@ -12,6 +13,7 @@ import { buildStructuredData } from "../../seo/jsonld.ts";
|
|
|
12
13
|
import Analytics from "./Analytics.astro";
|
|
13
14
|
import Banner from "./Banner.astro";
|
|
14
15
|
import Breadcrumbs from "./Breadcrumbs.astro";
|
|
16
|
+
import Empty from "./Empty.astro";
|
|
15
17
|
import Favicon from "./Favicon.astro";
|
|
16
18
|
import Fonts from "./Fonts.astro";
|
|
17
19
|
import { bannerInitScript, themeInitScript } from "./head-scripts.ts";
|
|
@@ -108,9 +110,20 @@ interface Props {
|
|
|
108
110
|
/**
|
|
109
111
|
* User layout-slot overrides from `components.ts` (`defineComponents`). Each
|
|
110
112
|
* key replaces the matching built-in; unknown keys are ignored. Wired slots:
|
|
111
|
-
* `Header`, `
|
|
113
|
+
* `Header`, `Logo`, `Search`, `Sidebar`, `MobileNav`, `Breadcrumbs`,
|
|
114
|
+
* `TableOfContents`, `Pagination`, `Feedback`, plus the content-injection slots
|
|
115
|
+
* `PageHeader`, `PageFooter`, and `Footer` (which have no built-in and render
|
|
116
|
+
* only when overridden). `Layout` (replacing this whole component) is wired in
|
|
117
|
+
* the generated page templates, not here.
|
|
112
118
|
*/
|
|
113
119
|
layout?: Record<string, ComponentOverride>;
|
|
120
|
+
/**
|
|
121
|
+
* Snapshot serialized for React island hooks (`blume/hooks`). Rendered into a
|
|
122
|
+
* JSON `<script>` when present; omitted for zero-JS pages that need no islands.
|
|
123
|
+
*/
|
|
124
|
+
clientData?: BlumeClientData | null;
|
|
125
|
+
/** Table-of-contents settings (`toc` config): visibility + heading range. */
|
|
126
|
+
toc?: { enabled: boolean; maxLevel: number; minLevel: number };
|
|
114
127
|
}
|
|
115
128
|
|
|
116
129
|
const {
|
|
@@ -152,21 +165,44 @@ const {
|
|
|
152
165
|
xDefault,
|
|
153
166
|
localeSwitch,
|
|
154
167
|
layout = {},
|
|
168
|
+
clientData,
|
|
169
|
+
toc = { enabled: true, maxLevel: 3, minLevel: 2 },
|
|
155
170
|
} = Astro.props;
|
|
156
171
|
|
|
172
|
+
// Serialized once for island hooks; `<` escaped so content can't break the tag.
|
|
173
|
+
const clientDataJson = clientData
|
|
174
|
+
? JSON.stringify(clientData).replaceAll("<", "\\u003c")
|
|
175
|
+
: null;
|
|
176
|
+
|
|
157
177
|
// Layout-slot overrides: render the user's component when configured, else the
|
|
158
178
|
// built-in. Each override receives the same props as the component it replaces.
|
|
179
|
+
// `Logo` and `Search` are resolved inside the header (forwarded via `layout`).
|
|
159
180
|
const HeaderSlot = resolveSlot(layout.Header, Header);
|
|
160
181
|
const SidebarSlot = resolveSlot(layout.Sidebar, NavTree);
|
|
161
182
|
const BreadcrumbsSlot = resolveSlot(layout.Breadcrumbs, Breadcrumbs);
|
|
162
183
|
const TableOfContentsSlot = resolveSlot(layout.TableOfContents, TableOfContents);
|
|
163
184
|
const PaginationSlot = resolveSlot(layout.Pagination, Pagination);
|
|
185
|
+
// The mobile drawer reuses the sidebar unless a distinct `MobileNav` is given.
|
|
186
|
+
const MobileNavSlot = layout.MobileNav
|
|
187
|
+
? resolveSlot(layout.MobileNav, NavTree)
|
|
188
|
+
: null;
|
|
189
|
+
// Content-injection slots with no built-in: the `Empty` fallback renders nothing
|
|
190
|
+
// when unconfigured, so these are safe to render unconditionally.
|
|
191
|
+
const FooterSlot = resolveSlot(layout.Footer, Empty);
|
|
192
|
+
const PageHeaderSlot = resolveSlot(layout.PageHeader, Empty);
|
|
193
|
+
const PageFooterSlot = resolveSlot(layout.PageFooter, Empty);
|
|
194
|
+
// The "Was this page helpful?" rating (rendered only when `feedback` is on).
|
|
195
|
+
const FeedbackSlot = resolveSlot(layout.Feedback, PageFeedback);
|
|
164
196
|
|
|
165
197
|
const strings = ui ?? EN_UI;
|
|
166
198
|
// Filter search to the active language only when the site is multi-locale.
|
|
167
199
|
const searchLocale =
|
|
168
200
|
localeSwitch && localeSwitch.length > 1 ? locale : undefined;
|
|
169
|
-
|
|
201
|
+
// TOC entries: the configured heading range, or none when the TOC is disabled
|
|
202
|
+
// (an empty list makes TableOfContents render nothing).
|
|
203
|
+
const tocHeadings = toc.enabled
|
|
204
|
+
? headings.filter((h) => h.depth >= toc.minLevel && h.depth <= toc.maxLevel)
|
|
205
|
+
: [];
|
|
170
206
|
const pageTitle = page.title ? `${page.title} - ${site.title}` : site.title;
|
|
171
207
|
const description = page.description ?? site.description;
|
|
172
208
|
|
|
@@ -282,6 +318,7 @@ const bannerScript = banner?.dismissible
|
|
|
282
318
|
<Banner banner={banner} />
|
|
283
319
|
<HeaderSlot
|
|
284
320
|
askEnabled={askEnabled}
|
|
321
|
+
layout={layout}
|
|
285
322
|
localeSwitch={localeSwitch}
|
|
286
323
|
logo={logo}
|
|
287
324
|
navigation={navigation}
|
|
@@ -300,7 +337,20 @@ const bannerScript = banner?.dismissible
|
|
|
300
337
|
class="fixed top-16 start-0 z-[35] h-[calc(100dvh-4rem)] w-64 max-w-[80vw] -translate-x-[105%] overflow-y-auto border-border border-e bg-background px-5 pt-4 pb-6 transition-transform rtl:translate-x-[105%] [:where([data-blume-nav-open])_&]:translate-x-0! lg:sticky lg:z-auto lg:w-auto lg:max-w-none lg:translate-x-0! lg:border-e-0 lg:bg-transparent lg:px-4"
|
|
301
338
|
>
|
|
302
339
|
<nav>
|
|
303
|
-
|
|
340
|
+
{
|
|
341
|
+
MobileNavSlot ? (
|
|
342
|
+
<>
|
|
343
|
+
<div class="lg:hidden">
|
|
344
|
+
<MobileNavSlot currentRoute={page.route} items={sidebar} />
|
|
345
|
+
</div>
|
|
346
|
+
<div class="hidden lg:block">
|
|
347
|
+
<SidebarSlot currentRoute={page.route} items={sidebar} />
|
|
348
|
+
</div>
|
|
349
|
+
</>
|
|
350
|
+
) : (
|
|
351
|
+
<SidebarSlot currentRoute={page.route} items={sidebar} />
|
|
352
|
+
)
|
|
353
|
+
}
|
|
304
354
|
</nav>
|
|
305
355
|
</aside>
|
|
306
356
|
<main class="min-w-0 px-6 pt-6 pb-10 lg:px-8 xl:px-10" id="blume-content">
|
|
@@ -310,9 +360,11 @@ const bannerScript = banner?.dismissible
|
|
|
310
360
|
title={strings.toc.title}
|
|
311
361
|
variant="mobile"
|
|
312
362
|
/>
|
|
363
|
+
<PageHeaderSlot headings={tocHeadings} page={page} route={page.route} />
|
|
313
364
|
<article class="prose mx-auto max-w-[42rem]" dir={contentDir}>
|
|
314
365
|
<slot />
|
|
315
366
|
</article>
|
|
367
|
+
<PageFooterSlot headings={tocHeadings} page={page} route={page.route} />
|
|
316
368
|
{
|
|
317
369
|
formattedLastModified && (
|
|
318
370
|
<p class="mx-auto mt-10 max-w-[42rem] text-muted-foreground text-sm">
|
|
@@ -320,7 +372,7 @@ const bannerScript = banner?.dismissible
|
|
|
320
372
|
</p>
|
|
321
373
|
)
|
|
322
374
|
}
|
|
323
|
-
{feedback && <
|
|
375
|
+
{feedback && <FeedbackSlot strings={strings.feedback} />}
|
|
324
376
|
<PaginationSlot next={next} prev={prev} strings={strings.page} />
|
|
325
377
|
</main>
|
|
326
378
|
<aside
|
|
@@ -344,6 +396,17 @@ const bannerScript = banner?.dismissible
|
|
|
344
396
|
/>
|
|
345
397
|
</aside>
|
|
346
398
|
</div>
|
|
399
|
+
<FooterSlot navigation={navigation} site={site} ui={strings} />
|
|
400
|
+
{
|
|
401
|
+
clientDataJson && (
|
|
402
|
+
<script
|
|
403
|
+
id="blume-client-data"
|
|
404
|
+
is:inline
|
|
405
|
+
set:html={clientDataJson}
|
|
406
|
+
type="application/json"
|
|
407
|
+
/>
|
|
408
|
+
)
|
|
409
|
+
}
|
|
347
410
|
<button
|
|
348
411
|
aria-label="Close navigation"
|
|
349
412
|
class="fixed inset-0 z-[30] hidden cursor-pointer border-0 bg-black/40 [:where([data-blume-nav-open])_&]:block lg:hidden"
|
|
@@ -358,6 +421,9 @@ const bannerScript = banner?.dismissible
|
|
|
358
421
|
// Registers the <blume-toc> custom element: scrollspy for the table of
|
|
359
422
|
// contents, highlighting the section currently in view as you scroll.
|
|
360
423
|
import "./toc-element.ts";
|
|
424
|
+
// Dev-only: adds a friendly hint after a React island hydration mismatch.
|
|
425
|
+
// Tree-shaken out of production builds.
|
|
426
|
+
import "./hydration-hint.ts";
|
|
361
427
|
|
|
362
428
|
const svg = (name: string) =>
|
|
363
429
|
`<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${icons[name]}</svg>`;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Dev-only: when React reports a hydration mismatch for an island, follow it
|
|
2
|
+
// with a friendly Blume hint pointing at the islands guide. Guarded by
|
|
3
|
+
// `import.meta.env.DEV`, so the whole block is tree-shaken out of production
|
|
4
|
+
// builds (the module ships no runtime code there).
|
|
5
|
+
|
|
6
|
+
const HINT =
|
|
7
|
+
"[blume] A hydration mismatch was detected in an island. Make sure props passed from `.astro`/MDX are serializable and the component renders the same output on the server and client (avoid `Date.now()`, `Math.random()`, `window` at render time). See https://useblume.dev/docs/content/islands";
|
|
8
|
+
|
|
9
|
+
const PATTERNS = [
|
|
10
|
+
/hydrat/iu,
|
|
11
|
+
/did not match/iu,
|
|
12
|
+
/server-rendered html/iu,
|
|
13
|
+
/server rendered html/iu,
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
if (import.meta.env.DEV && typeof window !== "undefined") {
|
|
17
|
+
const original = console.error.bind(console);
|
|
18
|
+
let shown = false;
|
|
19
|
+
console.error = (...args: unknown[]) => {
|
|
20
|
+
original(...args);
|
|
21
|
+
if (shown) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const text = args.map(String).join(" ");
|
|
25
|
+
if (PATTERNS.some((pattern) => pattern.test(text))) {
|
|
26
|
+
shown = true;
|
|
27
|
+
original(HINT);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -4,10 +4,12 @@ import type { ComponentOverride } from "../../core/define-components.ts";
|
|
|
4
4
|
* Resolve a layout-slot override to the component Astro should render, falling
|
|
5
5
|
* back to Blume's built-in when no usable override is configured.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* By the time values reach here, the generated `components.ts` has already turned
|
|
8
|
+
* path strings and hydrated (`client:*`) overrides into imported components /
|
|
9
|
+
* wrappers, so the runtime map holds real components. This handles the remaining
|
|
10
|
+
* cases: a bare component reference, an `IslandDescriptor` (`{ component }`,
|
|
11
|
+
* unwrapped to its component), and — as a safety net for overrides that couldn't
|
|
12
|
+
* be resolved at build time — a leftover string, which falls back to the built-in.
|
|
11
13
|
*/
|
|
12
14
|
export const resolveSlot = <T>(
|
|
13
15
|
override: ComponentOverride | undefined,
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Public prop types for Blume's built-in components, so users can type their
|
|
2
|
+
// overrides and wrappers: `import type { CalloutProps } from "blume/components"`.
|
|
3
|
+
//
|
|
4
|
+
// Each type is derived straight from the component with Astro's `ComponentProps`,
|
|
5
|
+
// so it can never drift from the real props. `typeof import(...)` keeps these
|
|
6
|
+
// type-only (no runtime import); the file-level disable is because that syntax is
|
|
7
|
+
// the only way to reference an `.astro` component's type without a value import.
|
|
8
|
+
// oxlint-disable typescript/consistent-type-imports
|
|
9
|
+
import type { ComponentProps } from "astro/types";
|
|
10
|
+
|
|
11
|
+
export type AccordionProps = ComponentProps<
|
|
12
|
+
typeof import("./content/Accordion.astro").default
|
|
13
|
+
>;
|
|
14
|
+
export type AccordionItemProps = ComponentProps<
|
|
15
|
+
typeof import("./content/AccordionItem.astro").default
|
|
16
|
+
>;
|
|
17
|
+
export type BadgeProps = ComponentProps<
|
|
18
|
+
typeof import("./content/Badge.astro").default
|
|
19
|
+
>;
|
|
20
|
+
export type CalloutProps = ComponentProps<
|
|
21
|
+
typeof import("./content/Callout.astro").default
|
|
22
|
+
>;
|
|
23
|
+
export type CardProps = ComponentProps<
|
|
24
|
+
typeof import("./content/Card.astro").default
|
|
25
|
+
>;
|
|
26
|
+
export type CardGroupProps = ComponentProps<
|
|
27
|
+
typeof import("./content/CardGroup.astro").default
|
|
28
|
+
>;
|
|
29
|
+
export type CodeGroupProps = ComponentProps<
|
|
30
|
+
typeof import("./content/CodeGroup.astro").default
|
|
31
|
+
>;
|
|
32
|
+
export type ColumnProps = ComponentProps<
|
|
33
|
+
typeof import("./content/Column.astro").default
|
|
34
|
+
>;
|
|
35
|
+
export type ColumnsProps = ComponentProps<
|
|
36
|
+
typeof import("./content/Columns.astro").default
|
|
37
|
+
>;
|
|
38
|
+
export type ExpandableProps = ComponentProps<
|
|
39
|
+
typeof import("./content/Expandable.astro").default
|
|
40
|
+
>;
|
|
41
|
+
export type FrameProps = ComponentProps<
|
|
42
|
+
typeof import("./content/Frame.astro").default
|
|
43
|
+
>;
|
|
44
|
+
export type PanelProps = ComponentProps<
|
|
45
|
+
typeof import("./content/Panel.astro").default
|
|
46
|
+
>;
|
|
47
|
+
export type PromptProps = ComponentProps<
|
|
48
|
+
typeof import("./content/Prompt.astro").default
|
|
49
|
+
>;
|
|
50
|
+
export type StepProps = ComponentProps<
|
|
51
|
+
typeof import("./content/Step.astro").default
|
|
52
|
+
>;
|
|
53
|
+
export type StepsProps = ComponentProps<
|
|
54
|
+
typeof import("./content/Steps.astro").default
|
|
55
|
+
>;
|
|
56
|
+
export type TabProps = ComponentProps<
|
|
57
|
+
typeof import("./content/Tab.astro").default
|
|
58
|
+
>;
|
|
59
|
+
export type TabsProps = ComponentProps<
|
|
60
|
+
typeof import("./content/Tabs.astro").default
|
|
61
|
+
>;
|
|
62
|
+
export type TileProps = ComponentProps<
|
|
63
|
+
typeof import("./content/Tile.astro").default
|
|
64
|
+
>;
|
|
65
|
+
export type TooltipProps = ComponentProps<
|
|
66
|
+
typeof import("./content/Tooltip.astro").default
|
|
67
|
+
>;
|
|
68
|
+
export type YouTubeProps = ComponentProps<
|
|
69
|
+
typeof import("./content/YouTube.astro").default
|
|
70
|
+
>;
|
|
71
|
+
export type IconProps = ComponentProps<typeof import("./Icon.astro").default>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { join } from "pathe";
|
|
2
|
+
|
|
3
|
+
/** A static directory served at a URL prefix, in addition to `public/`. */
|
|
4
|
+
export interface AssetMount {
|
|
5
|
+
/** Absolute filesystem path to the source directory (or file). */
|
|
6
|
+
dir: string;
|
|
7
|
+
/** URL path prefix the source is served at, e.g. `/images`. */
|
|
8
|
+
url: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Resolve `content.assets` entries (top-level dirs served at the site root,
|
|
13
|
+
* alongside `public/`) to `{ dir, url }` mounts. Shared by the generated Astro
|
|
14
|
+
* runtime (dev middleware + build copy) and by link validation, so all three
|
|
15
|
+
* agree on where a `/images/foo.png` reference resolves on disk.
|
|
16
|
+
*
|
|
17
|
+
* Each entry is normalized to a leading-slash URL and joined to the project
|
|
18
|
+
* root; leading `./` or `/` and any `..` segments are stripped so a mount can't
|
|
19
|
+
* escape the root or collide with the site's own routing prefix.
|
|
20
|
+
*/
|
|
21
|
+
export const resolveAssetMounts = (
|
|
22
|
+
root: string,
|
|
23
|
+
assets: string[]
|
|
24
|
+
): AssetMount[] =>
|
|
25
|
+
assets.map((entry) => {
|
|
26
|
+
const rel = entry
|
|
27
|
+
.replace(/^[./]+/u, "")
|
|
28
|
+
.replaceAll(/\.\.\/?/gu, "")
|
|
29
|
+
.replace(/\/+$/u, "");
|
|
30
|
+
return { dir: join(root, rel), url: `/${rel}` };
|
|
31
|
+
});
|
package/src/core/bridge.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { readFile } from "node:fs/promises";
|
|
|
3
3
|
|
|
4
4
|
import { join } from "pathe";
|
|
5
5
|
|
|
6
|
+
import { assetSegments } from "../migrate/mintlify/assets.ts";
|
|
6
7
|
import { loadMintlifyConfig } from "../migrate/mintlify/config.ts";
|
|
7
8
|
import { mintlifyI18n } from "../migrate/mintlify/i18n.ts";
|
|
8
9
|
import type { BlumeConfig } from "./schema.ts";
|
|
@@ -64,11 +65,20 @@ export const detectMintlifyBridge = async (
|
|
|
64
65
|
const root_ = config.content?.root ?? ".";
|
|
65
66
|
const exclude = config.content?.exclude ?? [];
|
|
66
67
|
|
|
68
|
+
// Mintlify serves assets from the project root; the bridge never moves files,
|
|
69
|
+
// so referenced root-level asset folders (e.g. `images/`) are served in place
|
|
70
|
+
// via `content.assets` instead. This is the read-only twin of the migrator's
|
|
71
|
+
// relocation — same referenced segments, just no `public/` move.
|
|
72
|
+
const assets = assetSegments(config).filter(
|
|
73
|
+
(segment) => segment !== "public" && existsSync(join(root, segment))
|
|
74
|
+
);
|
|
75
|
+
|
|
67
76
|
return {
|
|
68
77
|
configFile,
|
|
69
78
|
raw: {
|
|
70
79
|
...config,
|
|
71
80
|
content: {
|
|
81
|
+
assets,
|
|
72
82
|
// Mirror the excludes onto `content.exclude` too: the generated Astro
|
|
73
83
|
// `docs` collection globs `content.root` (here the project root) and
|
|
74
84
|
// must skip node_modules/snippets just like the source does.
|