blume 0.1.2 → 0.1.4
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 +83 -8
- package/dist/cli/index.js.map +10 -10
- package/dist/types/core/i18n-ui.d.ts +46 -0
- package/dist/types/core/schema.d.ts +51 -45
- package/dist/types/core/types.d.ts +6 -0
- package/docs/advanced/custom-pages.mdx +38 -0
- package/docs/content/components.mdx +20 -0
- package/docs/content/navigation.mdx +12 -4
- package/package.json +1 -1
- package/src/astro/examples.ts +49 -12
- package/src/astro/generate.ts +36 -4
- package/src/astro/pages.ts +14 -0
- package/src/astro/templates.ts +48 -0
- package/src/components/content/Component.astro +5 -1
- package/src/components/layout/RootLayout.astro +13 -7
- package/src/components/layout/nav-utils.ts +64 -1
- package/src/core/i18n-ui.ts +9 -0
- package/src/core/navigation.ts +13 -1
- package/src/core/schema.ts +11 -5
- package/src/core/types.ts +6 -0
- package/src/registry/eject.ts +13 -2
package/src/astro/generate.ts
CHANGED
|
@@ -45,7 +45,7 @@ import { buildThemeCss } from "../theme/palette.ts";
|
|
|
45
45
|
import { twoslashCss } from "../theme/twoslash.ts";
|
|
46
46
|
import { discoverExamples } from "./examples.ts";
|
|
47
47
|
import { discoverIslands } from "./islands.ts";
|
|
48
|
-
import { customOgRoutes, discoverPages } from "./pages.ts";
|
|
48
|
+
import { customOgRoutes, discoverPages, routeIsTaken } from "./pages.ts";
|
|
49
49
|
import {
|
|
50
50
|
askEndpointTemplate,
|
|
51
51
|
astroConfigTemplate,
|
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
mcpEndpointTemplate,
|
|
62
62
|
mcpPageFile,
|
|
63
63
|
mixedbreadSearchEndpointTemplate,
|
|
64
|
+
notFoundPageTemplate,
|
|
64
65
|
ogEndpointTemplate,
|
|
65
66
|
rawMarkdownEndpointTemplate,
|
|
66
67
|
rssEndpointTemplate,
|
|
@@ -786,6 +787,25 @@ const writeMcpFiles = async (
|
|
|
786
787
|
]);
|
|
787
788
|
};
|
|
788
789
|
|
|
790
|
+
/**
|
|
791
|
+
* Write the default 404 page at Astro's reserved `src/pages/404.astro` path so
|
|
792
|
+
* static builds emit `dist/404.html`. Skipped when the project already owns
|
|
793
|
+
* `/404` (a custom `pages/404.astro` or a `404.md` content page), letting it be
|
|
794
|
+
* fully overridden without a route collision; `pruneOrphans` then removes any
|
|
795
|
+
* previously-generated copy.
|
|
796
|
+
*/
|
|
797
|
+
const writeNotFoundPage = async (
|
|
798
|
+
write: (path: string, content: string) => Promise<boolean>,
|
|
799
|
+
srcDir: string,
|
|
800
|
+
pages: { pattern: string }[],
|
|
801
|
+
contentPages: { route: string }[]
|
|
802
|
+
): Promise<void> => {
|
|
803
|
+
if (routeIsTaken(pages, contentPages, "/404")) {
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
await write(join(srcDir, "pages", "404.astro"), notFoundPageTemplate());
|
|
807
|
+
};
|
|
808
|
+
|
|
789
809
|
export interface GenerateResult {
|
|
790
810
|
/** Whether any structural file changed (config/page/content config). */
|
|
791
811
|
structuralChange: boolean;
|
|
@@ -985,6 +1005,9 @@ export const generateRuntime = async (
|
|
|
985
1005
|
);
|
|
986
1006
|
}
|
|
987
1007
|
|
|
1008
|
+
// The default 404 page (`/404`), unless the project already owns the route.
|
|
1009
|
+
await writeNotFoundPage(write, srcDir, pages, project.graph.pages);
|
|
1010
|
+
|
|
988
1011
|
// The provider-specific client loader behind the `blume:search-client` alias
|
|
989
1012
|
// is always (re)generated so the alias resolves even when search is disabled.
|
|
990
1013
|
await write(searchClientPath, searchClientTemplate(config));
|
|
@@ -1054,10 +1077,19 @@ export const generateRuntime = async (
|
|
|
1054
1077
|
...exampleDiscovery.warnings,
|
|
1055
1078
|
];
|
|
1056
1079
|
|
|
1057
|
-
//
|
|
1058
|
-
//
|
|
1080
|
+
// Provider SDKs are optional peers; warn (rather than fail opaquely in Vite)
|
|
1081
|
+
// when the configured provider's package isn't installed. A dep is available
|
|
1082
|
+
// if the project installed it (resolves from the root) OR Blume ships it
|
|
1083
|
+
// (resolves from the Blume package — the same set the `.blume` deps link
|
|
1084
|
+
// exposes to the build). Resolving from the project root alone falsely flagged
|
|
1085
|
+
// a shipped SDK like Orama (the default provider) as missing whenever it
|
|
1086
|
+
// wasn't hoisted into the project, e.g. under isolated linkers. We resolve
|
|
1087
|
+
// from each package's real location rather than through the `.blume` junction,
|
|
1088
|
+
// which can't be traversed reliably for store-symlinked deps.
|
|
1059
1089
|
for (const dep of searchProviderMeta(config.search.provider).runtimeDeps) {
|
|
1060
|
-
if (
|
|
1090
|
+
if (
|
|
1091
|
+
!(canResolveFrom(context.root, dep) || canResolveFrom(packageRoot(), dep))
|
|
1092
|
+
) {
|
|
1061
1093
|
warnings.push(
|
|
1062
1094
|
`Search provider "${config.search.provider}" needs "${dep}", which isn't installed. Run \`npm install ${dep}\` (or your package manager's equivalent).`
|
|
1063
1095
|
);
|
package/src/astro/pages.ts
CHANGED
|
@@ -27,6 +27,20 @@ export const discoverPages = async (
|
|
|
27
27
|
});
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Whether the project already owns `route` — through a custom `.astro` page
|
|
32
|
+
* (injected, so matched on `pattern`) or a content page (matched on `route`).
|
|
33
|
+
* Used to skip a generated default page (e.g. `/404`, `/changelog`) so a
|
|
34
|
+
* user-authored page overrides it without a route collision.
|
|
35
|
+
*/
|
|
36
|
+
export const routeIsTaken = (
|
|
37
|
+
pages: { pattern: string }[],
|
|
38
|
+
contentPages: { route: string }[],
|
|
39
|
+
route: string
|
|
40
|
+
): boolean =>
|
|
41
|
+
pages.some((page) => page.pattern === route) ||
|
|
42
|
+
contentPages.some((page) => page.route === route);
|
|
43
|
+
|
|
30
44
|
/** A custom-page route that should get a generated OG card. */
|
|
31
45
|
export interface OgCustomRoute {
|
|
32
46
|
/** `og/<slug>.png` path segment; `index` for the site root. */
|
package/src/astro/templates.ts
CHANGED
|
@@ -1188,6 +1188,54 @@ const canonical = base ? base + "/changelog" : null;
|
|
|
1188
1188
|
`;
|
|
1189
1189
|
};
|
|
1190
1190
|
|
|
1191
|
+
/**
|
|
1192
|
+
* Generate `.blume/src/pages/404.astro`: the default not-found page. Rendered
|
|
1193
|
+
* through `PageLayout` (header + search, no sidebar) so it stays consistent with
|
|
1194
|
+
* the rest of the site, with copy pulled from the translatable `notFound` UI
|
|
1195
|
+
* strings. Written at Astro's reserved `src/pages/404.astro` path so static
|
|
1196
|
+
* builds emit `dist/404.html` and the dev server serves it for unmatched routes.
|
|
1197
|
+
* Skipped by the generator when a user `pages/404.astro` already occupies the
|
|
1198
|
+
* `/404` route, so projects can fully override it.
|
|
1199
|
+
*/
|
|
1200
|
+
export const notFoundPageTemplate = (): string => `---
|
|
1201
|
+
// Generated by Blume. Do not edit. Override by adding \`pages/404.astro\`.
|
|
1202
|
+
import PageLayout from "blume/components/layout/PageLayout.astro";
|
|
1203
|
+
import data from "../generated/data.json";
|
|
1204
|
+
|
|
1205
|
+
export const prerender = true;
|
|
1206
|
+
|
|
1207
|
+
const nf = data.ui.notFound;
|
|
1208
|
+
---
|
|
1209
|
+
|
|
1210
|
+
<PageLayout
|
|
1211
|
+
site={{ title: data.config.title, description: data.config.description }}
|
|
1212
|
+
logo={data.config.logo}
|
|
1213
|
+
favicon={data.config.favicon}
|
|
1214
|
+
appleIcon={data.config.appleIcon}
|
|
1215
|
+
banner={data.config.banner}
|
|
1216
|
+
analytics={data.config.analytics}
|
|
1217
|
+
navigation={data.navigation}
|
|
1218
|
+
page={{ title: nf.title, route: "/404" }}
|
|
1219
|
+
themeMode={data.config.theme.mode}
|
|
1220
|
+
fontCssVars={data.fontCssVars}
|
|
1221
|
+
searchEnabled={data.config.search.enabled}
|
|
1222
|
+
ui={data.ui}
|
|
1223
|
+
noindex={true}
|
|
1224
|
+
>
|
|
1225
|
+
<div
|
|
1226
|
+
class="mx-auto flex min-h-[60vh] max-w-2xl flex-col items-center justify-center gap-4 px-6 py-24 text-center"
|
|
1227
|
+
>
|
|
1228
|
+
<p class="text-6xl font-bold text-muted-foreground">404</p>
|
|
1229
|
+
<h1 class="text-2xl font-semibold text-foreground">{nf.title}</h1>
|
|
1230
|
+
<p class="text-muted-foreground">{nf.description}</p>
|
|
1231
|
+
<a
|
|
1232
|
+
class="mt-2 rounded-md bg-accent px-4 py-2 text-sm font-medium text-accent-foreground"
|
|
1233
|
+
href="/">{nf.home}</a
|
|
1234
|
+
>
|
|
1235
|
+
</div>
|
|
1236
|
+
</PageLayout>
|
|
1237
|
+
`;
|
|
1238
|
+
|
|
1191
1239
|
/**
|
|
1192
1240
|
* Generate `.blume/src/generated/components.ts`, which re-exports the user's
|
|
1193
1241
|
* component overrides (or empty maps when no `components.ts` exists). Importing
|
|
@@ -46,7 +46,11 @@ const paneStyle = `height:${paneHeight}px`;
|
|
|
46
46
|
// CodeGroup, switching one Component must not switch the others.
|
|
47
47
|
<Tabs hash={false} sync={false}>
|
|
48
48
|
<Tab
|
|
49
|
-
|
|
49
|
+
// `not-prose`: the preview lives inside the page's `.prose` wrapper, so
|
|
50
|
+
// without this the typography styles bleed into the live component
|
|
51
|
+
// (headings, links, lists, spacing). The Code pane below keeps prose on
|
|
52
|
+
// purpose — that's what styles the highlighted source.
|
|
53
|
+
class="not-prose flex items-center justify-center overflow-auto"
|
|
50
54
|
style={paneStyle}
|
|
51
55
|
title="Preview"
|
|
52
56
|
>
|
|
@@ -16,7 +16,12 @@ import Favicon from "./Favicon.astro";
|
|
|
16
16
|
import Fonts from "./Fonts.astro";
|
|
17
17
|
import { bannerInitScript, themeInitScript } from "./head-scripts.ts";
|
|
18
18
|
import Header from "./Header.astro";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
findBreadcrumbs,
|
|
21
|
+
flattenPages,
|
|
22
|
+
getPagination,
|
|
23
|
+
sidebarForRoute,
|
|
24
|
+
} from "./nav-utils.ts";
|
|
20
25
|
import NavTree from "./NavTree.astro";
|
|
21
26
|
import { resolveSlot } from "./overrides.ts";
|
|
22
27
|
import PageActions from "./PageActions.astro";
|
|
@@ -179,11 +184,12 @@ const formattedLastModified =
|
|
|
179
184
|
// Needs a configured site to be useful, so the menu is hidden without one.
|
|
180
185
|
const mcpUrl = mcp && siteUrl ? new URL(mcp.route, siteUrl).href : null;
|
|
181
186
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
);
|
|
187
|
+
// Scope the sidebar (and the breadcrumbs/pagination derived from it) to the
|
|
188
|
+
// active tab's section, so a multi-section site drills each tab into its own
|
|
189
|
+
// pages. Without tabs — or on a route under none — this is the full sidebar.
|
|
190
|
+
const sidebar = sidebarForRoute(navigation.sidebar, navigation.tabs, page.route);
|
|
191
|
+
const crumbs = findBreadcrumbs(sidebar, page.route);
|
|
192
|
+
const { prev, next } = getPagination(flattenPages(sidebar), page.route);
|
|
187
193
|
|
|
188
194
|
// Structured data is skipped when disabled or for pages we ask crawlers not to
|
|
189
195
|
// index. Escape `<` so a title/description containing `</script>` can't break
|
|
@@ -294,7 +300,7 @@ const bannerScript = banner?.dismissible
|
|
|
294
300
|
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"
|
|
295
301
|
>
|
|
296
302
|
<nav>
|
|
297
|
-
<SidebarSlot currentRoute={page.route} items={
|
|
303
|
+
<SidebarSlot currentRoute={page.route} items={sidebar} />
|
|
298
304
|
</nav>
|
|
299
305
|
</aside>
|
|
300
306
|
<main class="min-w-0 px-6 pt-6 pb-10 lg:px-8 xl:px-10" id="blume-content">
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { NavNode } from "../../core/types.ts";
|
|
1
|
+
import type { NavNode, NavTab } from "../../core/types.ts";
|
|
2
2
|
|
|
3
3
|
/** A flat, ordered page reference used for previous/next pagination. */
|
|
4
4
|
export interface FlatPage {
|
|
@@ -71,6 +71,69 @@ export const findBreadcrumbs = (nodes: NavNode[], route: string): Crumb[] => {
|
|
|
71
71
|
return search(nodes, []) ?? [];
|
|
72
72
|
};
|
|
73
73
|
|
|
74
|
+
/** Whether `route` is the section root `base` or nested beneath it. */
|
|
75
|
+
const isUnderPath = (route: string, base: string): boolean =>
|
|
76
|
+
route === base || route.startsWith(`${base}/`);
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The tab whose `path` is the longest prefix of `route`, mirroring the header's
|
|
80
|
+
* active-tab highlight. The root tab (`/`) is skipped — it spans everything and
|
|
81
|
+
* so never scopes the sidebar.
|
|
82
|
+
*/
|
|
83
|
+
const activeTab = (tabs: NavTab[], route: string): NavTab | null => {
|
|
84
|
+
let match: NavTab | null = null;
|
|
85
|
+
for (const tab of tabs) {
|
|
86
|
+
if (tab.path === "/" || !isUnderPath(route, tab.path)) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!match || tab.path.length > match.path.length) {
|
|
90
|
+
match = tab;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return match;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The children of the group whose path is `base`, searched at any depth — so a
|
|
98
|
+
* content tree wrapped in a top-level container group still resolves to the
|
|
99
|
+
* right section. Returns null when no group sits exactly at `base`.
|
|
100
|
+
*/
|
|
101
|
+
const sectionChildren = (nodes: NavNode[], base: string): NavNode[] | null => {
|
|
102
|
+
for (const node of nodes) {
|
|
103
|
+
if (node.kind !== "group") {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (node.path === base || node.route === base) {
|
|
107
|
+
return node.children;
|
|
108
|
+
}
|
|
109
|
+
const deeper = sectionChildren(node.children, base);
|
|
110
|
+
if (deeper) {
|
|
111
|
+
return deeper;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Scope the sidebar to the active tab's section. With tabs configured, a route
|
|
119
|
+
* under one tab shows only that tab's group — so a multi-section site (e.g.
|
|
120
|
+
* Adapters / API / AI tabs) drills each tab into its own pages instead of one
|
|
121
|
+
* global tree, the way Fumadocs' root folders do. Falls back to the full
|
|
122
|
+
* sidebar when no tab matches (or the tab maps to no group), so a route is
|
|
123
|
+
* never left with a blank sidebar.
|
|
124
|
+
*/
|
|
125
|
+
export const sidebarForRoute = (
|
|
126
|
+
sidebar: NavNode[],
|
|
127
|
+
tabs: NavTab[],
|
|
128
|
+
route: string
|
|
129
|
+
): NavNode[] => {
|
|
130
|
+
const tab = activeTab(tabs, route);
|
|
131
|
+
if (!tab) {
|
|
132
|
+
return sidebar;
|
|
133
|
+
}
|
|
134
|
+
return sectionChildren(sidebar, tab.path) ?? sidebar;
|
|
135
|
+
};
|
|
136
|
+
|
|
74
137
|
/** Resolve previous/next pages around the current route. */
|
|
75
138
|
export const getPagination = (
|
|
76
139
|
flat: FlatPage[],
|
package/src/core/i18n-ui.ts
CHANGED
|
@@ -50,6 +50,15 @@ const uiStringsObject = z.object({
|
|
|
50
50
|
untranslated: z.string().default("Not translated"),
|
|
51
51
|
})
|
|
52
52
|
.default({}),
|
|
53
|
+
notFound: z
|
|
54
|
+
.object({
|
|
55
|
+
description: z
|
|
56
|
+
.string()
|
|
57
|
+
.default("We couldn't find the page you're looking for."),
|
|
58
|
+
home: z.string().default("Back to home"),
|
|
59
|
+
title: z.string().default("Page not found"),
|
|
60
|
+
})
|
|
61
|
+
.default({}),
|
|
53
62
|
page: z
|
|
54
63
|
.object({
|
|
55
64
|
lastUpdated: z.string().default("Last updated on"),
|
package/src/core/navigation.ts
CHANGED
|
@@ -55,6 +55,8 @@ interface MutableGroup {
|
|
|
55
55
|
kind: "group";
|
|
56
56
|
key: string;
|
|
57
57
|
path: string;
|
|
58
|
+
/** The group's URL path (folder route prefix); set as pages are inserted. */
|
|
59
|
+
routePath?: string;
|
|
58
60
|
label: string;
|
|
59
61
|
icon?: string;
|
|
60
62
|
collapsed?: boolean;
|
|
@@ -194,6 +196,7 @@ const toNavNode = (node: MutableNode): NavNode => {
|
|
|
194
196
|
icon: node.icon,
|
|
195
197
|
kind: "group",
|
|
196
198
|
label: node.label,
|
|
199
|
+
path: node.routePath,
|
|
197
200
|
};
|
|
198
201
|
};
|
|
199
202
|
|
|
@@ -215,9 +218,18 @@ const buildFileSystemSidebar = (
|
|
|
215
218
|
const filename = parts.at(-1) ?? page.navPath;
|
|
216
219
|
const dirs = parts.slice(0, -1);
|
|
217
220
|
|
|
221
|
+
// Each group's URL path is the matching prefix of the page's route. navPath
|
|
222
|
+
// is locale-stripped while the route may carry a locale/base prefix, so
|
|
223
|
+
// align the folder segments from the right (the extra leading segments are
|
|
224
|
+
// that prefix). Under such a prefix the path won't match a logical tab path,
|
|
225
|
+
// so tab-scoping simply no-ops — same as the header's active-tab logic.
|
|
226
|
+
const folderParts = page.route.split("/").filter(Boolean).slice(0, -1);
|
|
227
|
+
const offset = Math.max(0, folderParts.length - dirs.length);
|
|
228
|
+
|
|
218
229
|
let parent = root;
|
|
219
|
-
for (const dir of dirs) {
|
|
230
|
+
for (const [index, dir] of dirs.entries()) {
|
|
220
231
|
parent = ensureGroup(parent, dir);
|
|
232
|
+
parent.routePath ??= `/${folderParts.slice(0, offset + index + 1).join("/")}`;
|
|
221
233
|
}
|
|
222
234
|
|
|
223
235
|
parent.children.push({
|
package/src/core/schema.ts
CHANGED
|
@@ -1022,11 +1022,17 @@ export const blumeConfigSchema = z
|
|
|
1022
1022
|
deployment: deploymentConfigSchema.default({}),
|
|
1023
1023
|
description: z.string().optional(),
|
|
1024
1024
|
/**
|
|
1025
|
-
*
|
|
1026
|
-
*
|
|
1027
|
-
* elsewhere when examples live outside a top-level `examples
|
|
1028
|
-
*
|
|
1029
|
-
*
|
|
1025
|
+
* Where `<Component path>` resolves live previews and their source from,
|
|
1026
|
+
* relative to the project root. Defaults to the `examples` directory; point
|
|
1027
|
+
* it elsewhere when examples live outside a top-level `examples/`.
|
|
1028
|
+
*
|
|
1029
|
+
* May be a glob (anything with `*`/`?`/`[]`/`{}`/`!`), in which case only
|
|
1030
|
+
* matching files are discovered and a `<Component path>` key is relative to
|
|
1031
|
+
* the glob's static prefix. Use this for a registry layout that colocates
|
|
1032
|
+
* component sources with their examples — `registry/<pkg>/**\/examples/*`
|
|
1033
|
+
* targets just the examples, leaving the sources (which have no default
|
|
1034
|
+
* export to wrap) out, so the registry needn't be forked into its own
|
|
1035
|
+
* examples directory.
|
|
1030
1036
|
*/
|
|
1031
1037
|
examples: z.string().default("examples"),
|
|
1032
1038
|
export: exportConfigSchema.default(false),
|
package/src/core/types.ts
CHANGED
|
@@ -137,6 +137,12 @@ export type NavNode =
|
|
|
137
137
|
display?: SidebarDisplay;
|
|
138
138
|
icon?: string;
|
|
139
139
|
route?: string;
|
|
140
|
+
/**
|
|
141
|
+
* The group's URL path (its folder route prefix), even when the folder
|
|
142
|
+
* has no index page to link. Used to scope the sidebar to a tab's section;
|
|
143
|
+
* not a clickable link (that's `route`).
|
|
144
|
+
*/
|
|
145
|
+
path?: string;
|
|
140
146
|
collapsed?: boolean;
|
|
141
147
|
children: NavNode[];
|
|
142
148
|
};
|
package/src/registry/eject.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
detectNeedsReact,
|
|
13
13
|
} from "../astro/generate.ts";
|
|
14
14
|
import { discoverIslands } from "../astro/islands.ts";
|
|
15
|
-
import { customOgRoutes, discoverPages } from "../astro/pages.ts";
|
|
15
|
+
import { customOgRoutes, discoverPages, routeIsTaken } from "../astro/pages.ts";
|
|
16
16
|
import {
|
|
17
17
|
askEndpointTemplate,
|
|
18
18
|
astroConfigTemplate,
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
islandMapTemplate,
|
|
26
26
|
islandWrapperTemplate,
|
|
27
27
|
mixedbreadSearchEndpointTemplate,
|
|
28
|
+
notFoundPageTemplate,
|
|
28
29
|
ogEndpointTemplate,
|
|
29
30
|
rawMarkdownEndpointTemplate,
|
|
30
31
|
rssEndpointTemplate,
|
|
@@ -73,7 +74,7 @@ export const eject = async (root: string): Promise<string[]> => {
|
|
|
73
74
|
: Promise.resolve(""),
|
|
74
75
|
buildRawMarkdown(project),
|
|
75
76
|
discoverIslands(root),
|
|
76
|
-
discoverExamples(root),
|
|
77
|
+
discoverExamples(root, config.examples),
|
|
77
78
|
]);
|
|
78
79
|
// Island/example frameworks drive which Astro renderers the ejected config
|
|
79
80
|
// wires in; React also switches on for project `.tsx`/`.jsx` and Ask AI.
|
|
@@ -206,6 +207,16 @@ export const eject = async (root: string): Promise<string[]> => {
|
|
|
206
207
|
});
|
|
207
208
|
}
|
|
208
209
|
|
|
210
|
+
// Default 404 page, unless the project already owns `/404` (a custom
|
|
211
|
+
// `pages/404.astro` or a `404.md` content page). The ejected project owns the
|
|
212
|
+
// file afterwards and can edit or remove it.
|
|
213
|
+
if (!routeIsTaken(pages, project.graph.pages, "/404")) {
|
|
214
|
+
files.push({
|
|
215
|
+
content: notFoundPageTemplate(),
|
|
216
|
+
path: join(srcDir, "pages", "404.astro"),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
209
220
|
// The provider-specific client loader behind the `blume:search-client` alias.
|
|
210
221
|
files.push({
|
|
211
222
|
content: searchClientTemplate(config),
|