blume 1.6.6 → 1.7.1
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 +27 -0
- package/dist/cli/{chunk-etsqspj6.js → chunk-12dzsn9b.js} +140 -23
- package/dist/cli/{chunk-etsqspj6.js.map → chunk-12dzsn9b.js.map} +6 -5
- package/dist/cli/{chunk-aerwpe14.js → chunk-4ae4f395.js} +164 -51
- package/dist/cli/chunk-4ae4f395.js.map +15 -0
- package/dist/cli/{chunk-m3p3wahd.js → chunk-52cwcqvp.js} +4 -4
- package/dist/cli/{chunk-j00ezcg5.js → chunk-5gfw0q4j.js} +9 -9
- package/dist/cli/{chunk-bawgnt8x.js → chunk-6mq7qkve.js} +3 -3
- package/dist/cli/{chunk-nyqzjdhj.js → chunk-82atea4k.js} +5 -5
- package/dist/cli/{chunk-tc89yh2r.js → chunk-8p3xe5jv.js} +2 -2
- package/dist/cli/{chunk-s4k1pnvf.js → chunk-90pdhkpm.js} +11 -11
- package/dist/cli/{chunk-n0y172hf.js → chunk-aqjvpd03.js} +4 -4
- package/dist/cli/{chunk-x1vrdjyk.js → chunk-h9ekmtz7.js} +5 -5
- package/dist/cli/{chunk-f75cqye8.js → chunk-he2zfgah.js} +10 -10
- package/dist/cli/{chunk-s4jn7f1q.js → chunk-j5f2wrj5.js} +2 -2
- package/dist/cli/{chunk-wkq5tbtq.js → chunk-k0v1f8bb.js} +3 -3
- package/dist/cli/{chunk-n4qjabmt.js → chunk-ka5k7cz9.js} +6 -19
- package/dist/cli/{chunk-n4qjabmt.js.map → chunk-ka5k7cz9.js.map} +3 -4
- package/dist/cli/{chunk-5yvt556e.js → chunk-kmx2mydj.js} +2 -2
- package/dist/cli/{chunk-ag1zyr5x.js → chunk-mfm4sjwx.js} +11 -11
- package/dist/cli/{chunk-0ewz4trd.js → chunk-np8dmfb0.js} +6 -6
- package/dist/cli/{chunk-cnvm6k3e.js → chunk-pdwg3q9g.js} +11 -11
- package/dist/cli/{chunk-vv237fp3.js → chunk-q56730e0.js} +26 -12
- package/dist/cli/{chunk-vv237fp3.js.map → chunk-q56730e0.js.map} +3 -3
- package/dist/cli/{chunk-3k0kzs6d.js → chunk-qvvpnwaz.js} +2 -2
- package/dist/cli/{chunk-vv3f8mb6.js → chunk-r99hynxh.js} +25 -24
- package/dist/cli/{chunk-vv3f8mb6.js.map → chunk-r99hynxh.js.map} +4 -4
- package/dist/cli/{chunk-wb067mv3.js → chunk-vyqj481z.js} +19 -7
- package/dist/cli/chunk-vyqj481z.js.map +13 -0
- package/dist/cli/{chunk-62qsssnh.js → chunk-x1wvw7a8.js} +411 -145
- package/dist/cli/chunk-x1wvw7a8.js.map +40 -0
- package/dist/cli/{chunk-9sh49q0h.js → chunk-ywn7t0pb.js} +2 -2
- package/dist/cli/index.js +13 -13
- package/dist/types/components/layout/nav-utils.d.ts +46 -1
- package/dist/types/core/config-input.d.ts +7 -0
- package/dist/types/core/schema.d.ts +2 -0
- package/dist/types/theme/fonts.d.ts +22 -22
- package/docs/02-deployment.mdx +21 -0
- package/docs/08-faq.mdx +21 -0
- package/docs/configuration/ask-ai.mdx +16 -0
- package/docs/content/navigation.mdx +2 -0
- package/docs/content/sources.mdx +2 -2
- package/docs/content/syntax.mdx +1 -1
- package/docs/discoverability/open-graph.mdx +4 -0
- package/package.json +1 -1
- package/src/ai/ask.ts +31 -4
- package/src/astro/generate.ts +142 -5
- package/src/astro/integration.ts +12 -1
- package/src/astro/module-types.ts +9 -0
- package/src/astro/templates.ts +260 -56
- package/src/cli/commands/build.ts +28 -0
- package/src/components/Icon.astro +24 -0
- package/src/components/content/YouTube.astro +1 -1
- package/src/components/icon-sprite-middleware.ts +41 -0
- package/src/components/icon-sprite.ts +93 -0
- package/src/components/layout/Header.astro +1 -1
- package/src/components/layout/IconSprite.astro +11 -0
- package/src/components/layout/NavTree.astro +156 -188
- package/src/components/layout/NavTreeCache.astro +45 -0
- package/src/components/layout/NavTreeScript.astro +256 -0
- package/src/components/layout/PageActions.astro +11 -5
- package/src/components/layout/PageLayout.astro +7 -0
- package/src/components/layout/ReferenceLayout.astro +7 -0
- package/src/components/layout/RootLayout.astro +30 -2
- package/src/components/layout/Search.astro +11 -0
- package/src/components/layout/nav-cache.ts +49 -0
- package/src/components/layout/nav-utils.ts +87 -1
- package/src/core/config-input.ts +7 -0
- package/src/core/schema.ts +5 -0
- package/src/core/sources/assets.ts +162 -26
- package/src/core/sources/notion.ts +60 -1
- package/src/markdown/language-icon.ts +64 -20
- package/src/markdown/mermaid.ts +11 -0
- package/src/og/cache.ts +236 -0
- package/src/og/card.ts +12 -4
- package/src/og/index.ts +8 -1
- package/src/registry/eject.ts +24 -8
- package/src/theme/entry.ts +50 -7
- package/src/theme/fonts.ts +30 -23
- package/dist/cli/chunk-62qsssnh.js.map +0 -36
- package/dist/cli/chunk-aerwpe14.js.map +0 -15
- package/dist/cli/chunk-wb067mv3.js.map +0 -13
- /package/dist/cli/{chunk-m3p3wahd.js.map → chunk-52cwcqvp.js.map} +0 -0
- /package/dist/cli/{chunk-j00ezcg5.js.map → chunk-5gfw0q4j.js.map} +0 -0
- /package/dist/cli/{chunk-bawgnt8x.js.map → chunk-6mq7qkve.js.map} +0 -0
- /package/dist/cli/{chunk-nyqzjdhj.js.map → chunk-82atea4k.js.map} +0 -0
- /package/dist/cli/{chunk-tc89yh2r.js.map → chunk-8p3xe5jv.js.map} +0 -0
- /package/dist/cli/{chunk-s4k1pnvf.js.map → chunk-90pdhkpm.js.map} +0 -0
- /package/dist/cli/{chunk-n0y172hf.js.map → chunk-aqjvpd03.js.map} +0 -0
- /package/dist/cli/{chunk-x1vrdjyk.js.map → chunk-h9ekmtz7.js.map} +0 -0
- /package/dist/cli/{chunk-f75cqye8.js.map → chunk-he2zfgah.js.map} +0 -0
- /package/dist/cli/{chunk-s4jn7f1q.js.map → chunk-j5f2wrj5.js.map} +0 -0
- /package/dist/cli/{chunk-wkq5tbtq.js.map → chunk-k0v1f8bb.js.map} +0 -0
- /package/dist/cli/{chunk-5yvt556e.js.map → chunk-kmx2mydj.js.map} +0 -0
- /package/dist/cli/{chunk-ag1zyr5x.js.map → chunk-mfm4sjwx.js.map} +0 -0
- /package/dist/cli/{chunk-0ewz4trd.js.map → chunk-np8dmfb0.js.map} +0 -0
- /package/dist/cli/{chunk-cnvm6k3e.js.map → chunk-pdwg3q9g.js.map} +0 -0
- /package/dist/cli/{chunk-3k0kzs6d.js.map → chunk-qvvpnwaz.js.map} +0 -0
- /package/dist/cli/{chunk-9sh49q0h.js.map → chunk-ywn7t0pb.js.map} +0 -0
package/src/astro/templates.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { BLUME_IGNORE_DIRS } from "../core/sources/watch.ts";
|
|
|
14
14
|
import { trimChar } from "../core/trim.ts";
|
|
15
15
|
import type { ProjectContext } from "../core/types.ts";
|
|
16
16
|
import { applyBaseToAstroRedirects } from "../deploy/redirects.ts";
|
|
17
|
+
import type { OgCache } from "../og/cache.ts";
|
|
17
18
|
import type { OgFont, OgFontFamilies } from "../og/card.ts";
|
|
18
19
|
import { hasScalarReferences } from "../openapi/references.ts";
|
|
19
20
|
import { searchProviderMeta } from "../search/providers.ts";
|
|
@@ -137,6 +138,18 @@ const resolveSessionOption = (deployment: {
|
|
|
137
138
|
? "\n session: false,"
|
|
138
139
|
: "";
|
|
139
140
|
|
|
141
|
+
/**
|
|
142
|
+
* A font weight as Astro's Fonts API spells it: a variable range is
|
|
143
|
+
* `"100 900"` there, where Blume's config (and Takumi's Google Fonts helper,
|
|
144
|
+
* which the OG cards use) write `"100..900"`. Astro treats the dotted form as
|
|
145
|
+
* an unknown discrete weight and loads nothing for it.
|
|
146
|
+
*/
|
|
147
|
+
const astroFontWeights = (weights: (number | string)[]): string =>
|
|
148
|
+
JSON.stringify(weights).replaceAll(
|
|
149
|
+
/(?<min>\d+)\.\.(?<max>\d+)/gu,
|
|
150
|
+
"$<min> $<max>"
|
|
151
|
+
);
|
|
152
|
+
|
|
140
153
|
/** The named imports the generated config pulls from `astro/config`. */
|
|
141
154
|
const astroConfigImportLine = (options: { hasFonts: boolean }): string => {
|
|
142
155
|
const names = ["defineConfig"];
|
|
@@ -389,13 +402,31 @@ interface OptimizeDepsConfig {
|
|
|
389
402
|
* absolute path (see `reactIntegration`), which that check never matches. See
|
|
390
403
|
* the optimizeDeps comment in the generated config for the failure this prevents.
|
|
391
404
|
*/
|
|
405
|
+
/**
|
|
406
|
+
* The client-side libraries a site needs, decided at generation time. A
|
|
407
|
+
* feature no page uses is left out of the module graph entirely (see
|
|
408
|
+
* {@link featuresTemplate}), so its library never enters the client bundle —
|
|
409
|
+
* Mermaid alone is over 3 MB of chunks (ELK, Cytoscape, KaTeX, every diagram
|
|
410
|
+
* type) and most of the build's client-bundle memory.
|
|
411
|
+
*/
|
|
412
|
+
export interface ClientFeatures {
|
|
413
|
+
/** `export.epub` is on: the EPUB generator's browser bundle is needed. */
|
|
414
|
+
epub: boolean;
|
|
415
|
+
/** Some page has a mermaid fence: the `<blume-mermaid>` element is needed. */
|
|
416
|
+
mermaid: boolean;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** Every feature on: what a checkout that predates the detection shipped. */
|
|
420
|
+
const ALL_CLIENT_FEATURES: ClientFeatures = { epub: true, mermaid: true };
|
|
421
|
+
|
|
392
422
|
const resolveOptimizeDeps = (options: {
|
|
393
423
|
aliases: Record<string, string> | undefined;
|
|
394
424
|
context: ProjectContext;
|
|
425
|
+
features: ClientFeatures;
|
|
395
426
|
needsReact: boolean;
|
|
396
427
|
reactCompilerPath: string | null | undefined;
|
|
397
428
|
}): OptimizeDepsConfig => {
|
|
398
|
-
const { context } = options;
|
|
429
|
+
const { context, features } = options;
|
|
399
430
|
const optimizeDepsEntries = [
|
|
400
431
|
...(context.pagesRoot ? [`${context.pagesRoot}/**/*.astro`] : []),
|
|
401
432
|
`${context.root}/islands/**/*.{jsx,svelte,tsx,vue}`,
|
|
@@ -404,8 +435,10 @@ const resolveOptimizeDeps = (options: {
|
|
|
404
435
|
.map((dir) => `${dir}/**/*.{astro,jsx,svelte,tsx,vue}`),
|
|
405
436
|
];
|
|
406
437
|
const optimizeDepsInclude = [
|
|
407
|
-
|
|
408
|
-
|
|
438
|
+
// Only the libraries the site's features actually import: pre-bundling
|
|
439
|
+
// Mermaid costs the dev server seconds at startup for nothing otherwise.
|
|
440
|
+
...(features.mermaid ? ["blume > mermaid"] : []),
|
|
441
|
+
...(features.epub ? ["blume > epub-gen-memory/bundle"] : []),
|
|
409
442
|
// Astro's own client-router/prefetch virtual modules are deliberately NOT
|
|
410
443
|
// forced in here: they read Vite `define`-injected constants
|
|
411
444
|
// (__PREFETCH_PREFETCH_ALL__ and friends) that a pre-bundled copy loses,
|
|
@@ -494,6 +527,10 @@ export const astroConfigTemplate = (options: {
|
|
|
494
527
|
examplesThemePath: string;
|
|
495
528
|
themePath: string;
|
|
496
529
|
searchClientPath: string;
|
|
530
|
+
/** The generated client-feature loaders (`blume:features`). */
|
|
531
|
+
featuresPath: string;
|
|
532
|
+
/** Which client features the site uses; every feature on when omitted. */
|
|
533
|
+
features?: ClientFeatures;
|
|
497
534
|
/**
|
|
498
535
|
* Where the runtime data modules (`blume:data`, the search index, …) live as
|
|
499
536
|
* JSON files, for a project with no CLI to publish them in memory (eject):
|
|
@@ -528,6 +565,8 @@ export const astroConfigTemplate = (options: {
|
|
|
528
565
|
contentRoutes,
|
|
529
566
|
examplesPath,
|
|
530
567
|
examplesThemePath,
|
|
568
|
+
features = ALL_CLIENT_FEATURES,
|
|
569
|
+
featuresPath,
|
|
531
570
|
generatedModulesDir,
|
|
532
571
|
needsSvelte,
|
|
533
572
|
needsVue,
|
|
@@ -549,6 +588,7 @@ export const astroConfigTemplate = (options: {
|
|
|
549
588
|
const { optimizeDepsEntries, optimizeDepsInclude } = resolveOptimizeDeps({
|
|
550
589
|
aliases: options.aliases,
|
|
551
590
|
context,
|
|
591
|
+
features,
|
|
552
592
|
needsReact,
|
|
553
593
|
reactCompilerPath: options.reactCompilerPath,
|
|
554
594
|
});
|
|
@@ -666,9 +706,7 @@ export const astroConfigTemplate = (options: {
|
|
|
666
706
|
font.name
|
|
667
707
|
)}, cssVariable: ${JSON.stringify(
|
|
668
708
|
font.cssVariable
|
|
669
|
-
)}, weights: ${JSON.stringify(
|
|
670
|
-
font.weights
|
|
671
|
-
)}, subsets: ${JSON.stringify(
|
|
709
|
+
)}, weights: ${astroFontWeights(font.weights)}, subsets: ${JSON.stringify(
|
|
672
710
|
font.subsets
|
|
673
711
|
)}, fallbacks: ${JSON.stringify(font.fallbacks)} }`
|
|
674
712
|
)
|
|
@@ -750,7 +788,8 @@ export const astroConfigTemplate = (options: {
|
|
|
750
788
|
} = renderIntegrationBridge(options.integrationBridge);
|
|
751
789
|
|
|
752
790
|
return `// Generated by Blume. Do not edit; this file is recreated on each run.
|
|
753
|
-
${configSourceMarker}${userConfigImports}
|
|
791
|
+
${configSourceMarker}${userConfigImports}import { availableParallelism } from "node:os";
|
|
792
|
+
${defineConfigImport}
|
|
754
793
|
import mdx from "@astrojs/mdx";
|
|
755
794
|
import tailwindcss from "@tailwindcss/vite";
|
|
756
795
|
import { blumeMarkdownProcessor, blumeMdxProcessor, blumeShikiTransformers, blumeTwoslashTransformer } from "blume/markdown";
|
|
@@ -795,6 +834,12 @@ ${userConfigSetup}export default defineConfig({
|
|
|
795
834
|
// request latency behind the user's intent, so most navigations swap
|
|
796
835
|
// instantly.
|
|
797
836
|
prefetch: { prefetchAll: true },
|
|
837
|
+
// Prerender several pages at once. Rendering is single-threaded, but a
|
|
838
|
+
// page's OG card renders on a native thread and its HTML is written
|
|
839
|
+
// asynchronously, so the main thread would otherwise idle behind each
|
|
840
|
+
// page's off-thread work. Capped at 8: the gain flattens there and beyond it
|
|
841
|
+
// the overlap only adds memory.
|
|
842
|
+
build: { concurrency: Math.min(8, availableParallelism()) },
|
|
798
843
|
vite: {${viteCacheOption}
|
|
799
844
|
plugins: [${runtimeModulesPluginEntry}tailwindcss(), includeHmrPlugin(${JSON.stringify(
|
|
800
845
|
`${context.outDir}/src/generated/includes.json`
|
|
@@ -847,6 +892,7 @@ ${userConfigSetup}export default defineConfig({
|
|
|
847
892
|
"blume:ask": ${JSON.stringify(askPath)},
|
|
848
893
|
"blume:examples": ${JSON.stringify(examplesPath)},
|
|
849
894
|
"blume:examples-theme": ${JSON.stringify(examplesThemePath)},
|
|
895
|
+
"blume:features": ${JSON.stringify(featuresPath)},
|
|
850
896
|
"blume:search-client": ${JSON.stringify(searchClientPath)},
|
|
851
897
|
"blume:theme": ${JSON.stringify(themePath)},${runtimeModuleAliasLines}${userAliasLines}
|
|
852
898
|
},
|
|
@@ -1034,16 +1080,23 @@ export const askEndpointTemplate = (
|
|
|
1034
1080
|
];
|
|
1035
1081
|
let setup = "";
|
|
1036
1082
|
let modelExpr = JSON.stringify(backend.model);
|
|
1083
|
+
// `ai.ask.headers`, inlined as literals: every provider factory below takes
|
|
1084
|
+
// the same `headers` option, so one line serves all three.
|
|
1085
|
+
const headersField = backend.headers
|
|
1086
|
+
? `\n headers: ${JSON.stringify(backend.headers)},`
|
|
1087
|
+
: "";
|
|
1037
1088
|
if (backend.kind === "gateway") {
|
|
1038
|
-
setup = `\nconst gateway = createGateway({
|
|
1089
|
+
setup = `\nconst gateway = createGateway({
|
|
1090
|
+
apiKey: getSecret("AI_GATEWAY_API_KEY"),${headersField}
|
|
1091
|
+
});\n`;
|
|
1039
1092
|
modelExpr = `gateway(${JSON.stringify(backend.model)})`;
|
|
1040
1093
|
} else if (backend.kind === "openrouter") {
|
|
1041
1094
|
imports.push(
|
|
1042
1095
|
'import { createOpenRouter } from "@openrouter/ai-sdk-provider";'
|
|
1043
1096
|
);
|
|
1044
|
-
setup = `\nconst openrouter = createOpenRouter({
|
|
1045
|
-
|
|
1046
|
-
|
|
1097
|
+
setup = `\nconst openrouter = createOpenRouter({
|
|
1098
|
+
apiKey: getSecret(${JSON.stringify(backend.apiKeyEnv)}),${headersField}
|
|
1099
|
+
});\n`;
|
|
1047
1100
|
modelExpr = `openrouter(${JSON.stringify(backend.model)})`;
|
|
1048
1101
|
} else if (backend.kind === "openai-compatible") {
|
|
1049
1102
|
imports.push(
|
|
@@ -1051,7 +1104,7 @@ export const askEndpointTemplate = (
|
|
|
1051
1104
|
);
|
|
1052
1105
|
setup = `\nconst provider = createOpenAICompatible({
|
|
1053
1106
|
apiKey: getSecret(${JSON.stringify(backend.apiKeyEnv)}),
|
|
1054
|
-
baseURL: ${JSON.stringify(backend.baseUrl)}
|
|
1107
|
+
baseURL: ${JSON.stringify(backend.baseUrl)},${headersField}
|
|
1055
1108
|
name: ${JSON.stringify(backend.name)},
|
|
1056
1109
|
});\n`;
|
|
1057
1110
|
modelExpr = `provider(${JSON.stringify(backend.model)})`;
|
|
@@ -1289,6 +1342,34 @@ const hostedSearchOptions = (
|
|
|
1289
1342
|
}
|
|
1290
1343
|
};
|
|
1291
1344
|
|
|
1345
|
+
/**
|
|
1346
|
+
* Generate `.blume/src/generated/features.ts` — the client-feature loaders
|
|
1347
|
+
* behind the `blume:features` alias. Each loader is a dynamic import when the
|
|
1348
|
+
* site uses the feature and `null` otherwise, so an unused library is absent
|
|
1349
|
+
* from the module graph (never bundled, never pre-bundled in dev) rather than
|
|
1350
|
+
* merely lazy. Regenerated on every pass: a page that gains a mermaid fence
|
|
1351
|
+
* turns the loader on.
|
|
1352
|
+
*/
|
|
1353
|
+
export const featuresTemplate = (features: ClientFeatures): string =>
|
|
1354
|
+
`// Generated by Blume. Do not edit.
|
|
1355
|
+
//
|
|
1356
|
+
// The client features this site uses. A feature no page needs stays out of
|
|
1357
|
+
// the module graph entirely: its loader is null and its library never enters
|
|
1358
|
+
// the client bundle.
|
|
1359
|
+
|
|
1360
|
+
/** Registers the <blume-mermaid> element; null when no page has a mermaid fence. */
|
|
1361
|
+
export const loadMermaid: (() => Promise<unknown>) | null = ${
|
|
1362
|
+
features.mermaid
|
|
1363
|
+
? '() => import("blume/components/content/mermaid-element.ts")'
|
|
1364
|
+
: "null"
|
|
1365
|
+
};
|
|
1366
|
+
|
|
1367
|
+
/** The EPUB generator's browser bundle; null when export.epub is off. */
|
|
1368
|
+
export const loadEpub:
|
|
1369
|
+
| (() => Promise<typeof import("epub-gen-memory/bundle")>)
|
|
1370
|
+
| null = ${features.epub ? '() => import("epub-gen-memory/bundle")' : "null"};
|
|
1371
|
+
`;
|
|
1372
|
+
|
|
1292
1373
|
/**
|
|
1293
1374
|
* Generate `.blume/src/generated/search-client.ts` — the provider-specific
|
|
1294
1375
|
* loader the `<Search>` component lazy-imports via the `blume:search-client`
|
|
@@ -1610,10 +1691,85 @@ export function GET({ props }: { props: { section: string } }) {
|
|
|
1610
1691
|
}
|
|
1611
1692
|
`;
|
|
1612
1693
|
|
|
1694
|
+
/**
|
|
1695
|
+
* Generate the deferred sidebar fragments page
|
|
1696
|
+
* (`.blume/src/pages/blume-nav/[version]/[locale]/[id].astro`): one
|
|
1697
|
+
* prerendered partial per collapsible or drill-in group, holding just that
|
|
1698
|
+
* group's rows, which the sidebar fetches on first open instead of carrying
|
|
1699
|
+
* every collapsed section on every page. Ids are `navGroupIds` over the full
|
|
1700
|
+
* tree, the same ids the pages' sidebars use; the `version`/`locale`
|
|
1701
|
+
* segments pick the navigation variant (`current`/`default` for the
|
|
1702
|
+
* unversioned, unlocalized tree, and `default` again for the default locale
|
|
1703
|
+
* while its URL prefix is hidden: Astro's i18n routing 404s a page URL that
|
|
1704
|
+
* carries the default locale's code as a segment). Rendered with no current
|
|
1705
|
+
* route — a section that isn't on the active path has no active row by
|
|
1706
|
+
* definition.
|
|
1707
|
+
*/
|
|
1708
|
+
export const navFragmentTemplate = (): string =>
|
|
1709
|
+
`---
|
|
1710
|
+
// Generated by Blume. Do not edit.
|
|
1711
|
+
import NavTree from "blume/components/layout/NavTree.astro";
|
|
1712
|
+
import { withBase } from "blume/components/islands/base-path.ts";
|
|
1713
|
+
import {
|
|
1714
|
+
hiddenDefaultLocale,
|
|
1715
|
+
navGroupIds,
|
|
1716
|
+
navVariants,
|
|
1717
|
+
} from "blume/components/layout/nav-utils.ts";
|
|
1718
|
+
import type { NavNode } from "blume/core/types.ts";
|
|
1719
|
+
import data from "blume:data";
|
|
1720
|
+
|
|
1721
|
+
export const prerender = true;
|
|
1722
|
+
// A partial: no doctype, head, or script/style injection — the rows only.
|
|
1723
|
+
export const partial = true;
|
|
1724
|
+
|
|
1725
|
+
// Only imports are in scope here (Astro hoists getStaticPaths), which is why
|
|
1726
|
+
// the variant walk lives in nav-utils.
|
|
1727
|
+
export function getStaticPaths() {
|
|
1728
|
+
return navVariants(data, hiddenDefaultLocale(data.config.i18n)).flatMap(
|
|
1729
|
+
({ locale, navigation, version }) =>
|
|
1730
|
+
[...navGroupIds(navigation.sidebar)]
|
|
1731
|
+
.filter(([node]) => (node.display ?? "flat") !== "flat")
|
|
1732
|
+
.map(([, id]) => ({ params: { id, locale, version } }))
|
|
1733
|
+
);
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
const { id, locale, version } = Astro.params;
|
|
1737
|
+
const variant = navVariants(data, hiddenDefaultLocale(data.config.i18n)).find(
|
|
1738
|
+
(entry) => entry.version === version && entry.locale === locale
|
|
1739
|
+
);
|
|
1740
|
+
const ids = variant
|
|
1741
|
+
? navGroupIds(variant.navigation.sidebar)
|
|
1742
|
+
: new Map<NavNode, string>();
|
|
1743
|
+
const node = [...ids].find(([, groupId]) => groupId === id)?.[0];
|
|
1744
|
+
if (!(variant && node && node.kind === "group")) {
|
|
1745
|
+
return new Response(null, { status: 404 });
|
|
1746
|
+
}
|
|
1747
|
+
const strings =
|
|
1748
|
+
locale === "default" ? data.ui.nav : (data.uiByLocale[locale] ?? data.ui).nav;
|
|
1749
|
+
const fragmentBase = withBase(\`/blume-nav/\${version}/\${locale}\`);
|
|
1750
|
+
---
|
|
1751
|
+
|
|
1752
|
+
<NavTree
|
|
1753
|
+
currentRoute=""
|
|
1754
|
+
depth={1}
|
|
1755
|
+
fragmentBase={fragmentBase}
|
|
1756
|
+
idPrefix={id}
|
|
1757
|
+
ids={ids}
|
|
1758
|
+
items={node.children}
|
|
1759
|
+
root={false}
|
|
1760
|
+
strings={strings}
|
|
1761
|
+
/>
|
|
1762
|
+
`;
|
|
1763
|
+
|
|
1613
1764
|
/** Generate the OG image endpoint (`.blume/src/pages/og/[...slug].png.ts`). */
|
|
1614
1765
|
export const ogEndpointTemplate = (
|
|
1615
1766
|
customRoutes: OgCustomRoute[] = [],
|
|
1616
1767
|
og: {
|
|
1768
|
+
/**
|
|
1769
|
+
* The on-disk card cache (directory plus the rendering Blume version).
|
|
1770
|
+
* Absent, every card renders on every build.
|
|
1771
|
+
*/
|
|
1772
|
+
cache?: OgCache;
|
|
1617
1773
|
families?: OgFontFamilies;
|
|
1618
1774
|
fonts?: OgFont[];
|
|
1619
1775
|
/**
|
|
@@ -1626,8 +1782,8 @@ export const ogEndpointTemplate = (
|
|
|
1626
1782
|
includeChangelog = false
|
|
1627
1783
|
): string =>
|
|
1628
1784
|
`// Generated by Blume. Do not edit.
|
|
1629
|
-
import {
|
|
1630
|
-
import type { OgFont, OgFontFamilies } from "blume/og";
|
|
1785
|
+
import { cachedOgImage } from "blume/og";
|
|
1786
|
+
import type { OgCache, OgFont, OgFontFamilies } from "blume/og";
|
|
1631
1787
|
import data from "blume:data";
|
|
1632
1788
|
|
|
1633
1789
|
export const prerender = true;
|
|
@@ -1646,6 +1802,14 @@ const families: OgFontFamilies | undefined = ${
|
|
|
1646
1802
|
og.families ? JSON.stringify(og.families) : "undefined"
|
|
1647
1803
|
};
|
|
1648
1804
|
|
|
1805
|
+
// Rendered cards are kept on disk between builds, keyed by everything that
|
|
1806
|
+
// decides their pixels, so a rebuild renders only the cards whose title,
|
|
1807
|
+
// description, branding, or fonts changed. The directory is a build-machine
|
|
1808
|
+
// path, baked in for the same reason as the local font paths above.
|
|
1809
|
+
const cache: OgCache | undefined = ${
|
|
1810
|
+
og.cache ? JSON.stringify(og.cache) : "undefined"
|
|
1811
|
+
};
|
|
1812
|
+
|
|
1649
1813
|
// A page's own description (its \`seo.description\`, else \`description\`) is
|
|
1650
1814
|
// the card subtitle, so the image says what the page's og:description says.
|
|
1651
1815
|
// Pages without one fall back to the site-wide subtitle at render time.
|
|
@@ -1705,7 +1869,7 @@ const repoSlug = data.config.github
|
|
|
1705
1869
|
: undefined;
|
|
1706
1870
|
|
|
1707
1871
|
export async function GET({ props }: { props: CardProps }) {
|
|
1708
|
-
const png = await
|
|
1872
|
+
const png = await cachedOgImage(cache, {
|
|
1709
1873
|
accent: data.config.og.palette?.accent ?? data.config.theme.accent.light,
|
|
1710
1874
|
brand: data.config.title,
|
|
1711
1875
|
description: props.description ?? data.config.og.description,
|
|
@@ -1784,31 +1948,22 @@ const htmlLang = i18n ? i18n.defaultLocale : "en";
|
|
|
1784
1948
|
</ReferenceLayout>
|
|
1785
1949
|
`;
|
|
1786
1950
|
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1951
|
+
/**
|
|
1952
|
+
* The component map every rendered entry body receives via
|
|
1953
|
+
* `<Content components={...} />`: Blume's content components, `<Math>` when
|
|
1954
|
+
* math is enabled, the island wrappers, and the user's `mdxComponents`
|
|
1955
|
+
* overrides. Shared by the catch-all page and the changelog index so a
|
|
1956
|
+
* `:::` directive (compiled to `<Callout>`) or an explicit `<Steps>` renders
|
|
1957
|
+
* wherever an entry is rendered — an MDX body throws "Expected component
|
|
1958
|
+
* `X` to be defined" the moment a page renders it without this map.
|
|
1959
|
+
*/
|
|
1960
|
+
const contentComponentsSource = (mathEnabled: boolean) => {
|
|
1961
|
+
const mathImport = mathEnabled
|
|
1795
1962
|
? 'import Math from "blume/components/content/Math.astro";\n'
|
|
1796
1963
|
: "";
|
|
1797
|
-
const mathEntry =
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
? "\n clientData={{ config: data.config, navigation, page: { route, title: seo.title ?? title } }}"
|
|
1801
|
-
: "";
|
|
1802
|
-
|
|
1803
|
-
return `---
|
|
1804
|
-
// Generated by Blume. Do not edit.
|
|
1805
|
-
import { getEntry, render } from "astro:content";
|
|
1806
|
-
import type { CollectionKey } from "astro:content";
|
|
1807
|
-
import RootLayout from "blume/components/layout/RootLayout.astro";
|
|
1808
|
-
import { withBase } from "blume/components/islands/base-path.ts";
|
|
1809
|
-
import { stripBasePath, withBasePath } from "blume/core/base-path.ts";
|
|
1810
|
-
import { resolveSlot } from "blume/components/layout/overrides.ts";
|
|
1811
|
-
import Accordion from "blume/components/content/Accordion.astro";
|
|
1964
|
+
const mathEntry = mathEnabled ? "Math,\n " : "";
|
|
1965
|
+
return {
|
|
1966
|
+
imports: `import Accordion from "blume/components/content/Accordion.astro";
|
|
1812
1967
|
import AccordionItem from "blume/components/content/AccordionItem.astro";
|
|
1813
1968
|
import AutoTypeTable from "blume/components/content/AutoTypeTable.astro";
|
|
1814
1969
|
import Badge from "blume/components/content/Badge.astro";
|
|
@@ -1848,19 +2003,8 @@ import ApiOverview from "blume/components/openapi/ApiOverview.astro";
|
|
|
1848
2003
|
import ApiTagOperations from "blume/components/openapi/ApiTagOperations.astro";
|
|
1849
2004
|
import Operation from "blume/components/openapi/Operation.astro";
|
|
1850
2005
|
${mathImport}import { mdxComponents as userMdx, layoutOverrides } from "../generated/components.ts";
|
|
1851
|
-
import { islandComponents } from "../generated/islands.ts"
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
const Color = Object.assign(ColorRoot, { Item: ColorItem, Row: ColorRow });
|
|
1855
|
-
const Tree = Object.assign(TreeRoot, { File: TreeFile, Folder: TreeFolder });
|
|
1856
|
-
|
|
1857
|
-
// Docs content is file-based and always prerendered, even in server output
|
|
1858
|
-
// (where only endpoints like /api/ask render on demand). Without this, server
|
|
1859
|
-
// builds would render this route on demand and ignore getStaticPaths, leaving
|
|
1860
|
-
// the entry id undefined.
|
|
1861
|
-
export const prerender = true;
|
|
1862
|
-
|
|
1863
|
-
const components = {
|
|
2006
|
+
import { islandComponents } from "../generated/islands.ts";`,
|
|
2007
|
+
map: `{
|
|
1864
2008
|
Accordion,
|
|
1865
2009
|
AccordionItem,
|
|
1866
2010
|
ApiOverview,
|
|
@@ -1897,8 +2041,52 @@ const components = {
|
|
|
1897
2041
|
YouTube,
|
|
1898
2042
|
${mathEntry}...islandComponents,
|
|
1899
2043
|
...userMdx,
|
|
2044
|
+
}`,
|
|
2045
|
+
};
|
|
1900
2046
|
};
|
|
1901
2047
|
|
|
2048
|
+
export const catchAllPageTemplate = (options: {
|
|
2049
|
+
exportEpub: boolean;
|
|
2050
|
+
exportPdf: boolean;
|
|
2051
|
+
mathEnabled: boolean;
|
|
2052
|
+
/**
|
|
2053
|
+
* Whether the sidebar defers collapsed sections to prerendered fragments
|
|
2054
|
+
* (see `navFragmentTemplate`); true when some group renders as a
|
|
2055
|
+
* disclosure or a drill-in panel.
|
|
2056
|
+
*/
|
|
2057
|
+
navFragments?: boolean;
|
|
2058
|
+
/** Serialize the island-hooks snapshot; only needed when React is enabled. */
|
|
2059
|
+
needsReact: boolean;
|
|
2060
|
+
}): string => {
|
|
2061
|
+
const { imports: componentImports, map: componentMap } =
|
|
2062
|
+
contentComponentsSource(options.mathEnabled);
|
|
2063
|
+
// The island-hooks snapshot (config + navigation + page) for `blume/hooks`.
|
|
2064
|
+
const clientData = options.needsReact
|
|
2065
|
+
? "\n clientData={{ config: data.config, navigation, page: { route, title: seo.title ?? title } }}"
|
|
2066
|
+
: "";
|
|
2067
|
+
|
|
2068
|
+
return `---
|
|
2069
|
+
// Generated by Blume. Do not edit.
|
|
2070
|
+
import { getEntry, render } from "astro:content";
|
|
2071
|
+
import type { CollectionKey } from "astro:content";
|
|
2072
|
+
import RootLayout from "blume/components/layout/RootLayout.astro";
|
|
2073
|
+
import { withBase } from "blume/components/islands/base-path.ts";
|
|
2074
|
+
import { stripBasePath, withBasePath } from "blume/core/base-path.ts";
|
|
2075
|
+
import { resolveSlot } from "blume/components/layout/overrides.ts";
|
|
2076
|
+
${componentImports}
|
|
2077
|
+
import data from "blume:data";
|
|
2078
|
+
|
|
2079
|
+
const Color = Object.assign(ColorRoot, { Item: ColorItem, Row: ColorRow });
|
|
2080
|
+
const Tree = Object.assign(TreeRoot, { File: TreeFile, Folder: TreeFolder });
|
|
2081
|
+
|
|
2082
|
+
// Docs content is file-based and always prerendered, even in server output
|
|
2083
|
+
// (where only endpoints like /api/ask render on demand). Without this, server
|
|
2084
|
+
// builds would render this route on demand and ignore getStaticPaths, leaving
|
|
2085
|
+
// the entry id undefined.
|
|
2086
|
+
export const prerender = true;
|
|
2087
|
+
|
|
2088
|
+
const components = ${componentMap};
|
|
2089
|
+
|
|
1902
2090
|
export function getStaticPaths() {
|
|
1903
2091
|
return data.routes.map((route) => ({
|
|
1904
2092
|
params: { slug: route.path === "/" ? undefined : route.path.slice(1) },
|
|
@@ -2166,7 +2354,12 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
|
|
|
2166
2354
|
editUrl={editUrl}
|
|
2167
2355
|
feedback={data.config.feedback}
|
|
2168
2356
|
exportPdf={${options.exportPdf}}
|
|
2169
|
-
exportEpub={${options.exportEpub}}
|
|
2357
|
+
exportEpub={${options.exportEpub}}${
|
|
2358
|
+
options.navFragments
|
|
2359
|
+
? `
|
|
2360
|
+
navFragmentBase={withBase(\`/blume-nav/\${version || "current"}/\${i18n && localePrefix(locale) ? locale : "default"}\`)}`
|
|
2361
|
+
: ""
|
|
2362
|
+
}
|
|
2170
2363
|
openInChat={data.config.openInChat}
|
|
2171
2364
|
feeds={data.feeds}
|
|
2172
2365
|
discovery={data.config.discovery}
|
|
@@ -2195,6 +2388,7 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
|
|
|
2195
2388
|
export const changelogIndexTemplate = (options: {
|
|
2196
2389
|
exportEpub: boolean;
|
|
2197
2390
|
exportPdf: boolean;
|
|
2391
|
+
mathEnabled: boolean;
|
|
2198
2392
|
/** Serialize the island-hooks snapshot; only needed when React is enabled. */
|
|
2199
2393
|
needsReact: boolean;
|
|
2200
2394
|
/** Whether a `staged` collection exists (non-filesystem changelog sources). */
|
|
@@ -2208,6 +2402,8 @@ export const changelogIndexTemplate = (options: {
|
|
|
2208
2402
|
const stagedSpread = options.staged
|
|
2209
2403
|
? '\n ...(await getCollection("staged")),'
|
|
2210
2404
|
: "";
|
|
2405
|
+
const { imports: componentImports, map: componentMap } =
|
|
2406
|
+
contentComponentsSource(options.mathEnabled);
|
|
2211
2407
|
|
|
2212
2408
|
return `---
|
|
2213
2409
|
// Generated by Blume. Do not edit.
|
|
@@ -2217,11 +2413,19 @@ import Update from "blume/components/content/Update.astro";
|
|
|
2217
2413
|
import { withBase } from "blume/components/islands/base-path.ts";
|
|
2218
2414
|
import { resolveSlot } from "blume/components/layout/overrides.ts";
|
|
2219
2415
|
import { resolveDateFormatOptions } from "blume/core/date-format.ts";
|
|
2220
|
-
|
|
2416
|
+
${componentImports}
|
|
2221
2417
|
import data from "blume:data";
|
|
2222
2418
|
|
|
2419
|
+
const Color = Object.assign(ColorRoot, { Item: ColorItem, Row: ColorRow });
|
|
2420
|
+
const Tree = Object.assign(TreeRoot, { File: TreeFile, Folder: TreeFolder });
|
|
2421
|
+
|
|
2223
2422
|
export const prerender = true;
|
|
2224
2423
|
|
|
2424
|
+
// Entry bodies are MDX rendered outside the catch-all, so they need the same
|
|
2425
|
+
// component map: a \`:::\` callout or \`<Steps>\` in a release note otherwise
|
|
2426
|
+
// throws "Expected component ... to be defined" at build time.
|
|
2427
|
+
const components = ${componentMap};
|
|
2428
|
+
|
|
2225
2429
|
const entryDate = (entry: {
|
|
2226
2430
|
data: { date?: string | null; changelog?: { date?: string | null } | null };
|
|
2227
2431
|
}) => entry.data.date ?? entry.data.changelog?.date ?? null;
|
|
@@ -2420,7 +2624,7 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
|
|
|
2420
2624
|
>
|
|
2421
2625
|
{majorGroups[0].items.map(({ Content, href, id, label, date, tags }) => (
|
|
2422
2626
|
<Update description={date} href={href} id={id} label={label} tags={tags}>
|
|
2423
|
-
<Content />
|
|
2627
|
+
<Content components={components} />
|
|
2424
2628
|
</Update>
|
|
2425
2629
|
))}
|
|
2426
2630
|
{majorGroups.slice(1).map((group) => (
|
|
@@ -2431,7 +2635,7 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
|
|
|
2431
2635
|
>
|
|
2432
2636
|
{group.items.map(({ Content, href, id, label, date, tags }) => (
|
|
2433
2637
|
<Update description={date} href={href} id={id} label={label} tags={tags}>
|
|
2434
|
-
<Content />
|
|
2638
|
+
<Content components={components} />
|
|
2435
2639
|
</Update>
|
|
2436
2640
|
))}
|
|
2437
2641
|
</section>
|
|
@@ -2451,7 +2655,7 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
|
|
|
2451
2655
|
<div class="not-prose mt-8">
|
|
2452
2656
|
{items.map(({ Content, href, id, label, date, tags }) => (
|
|
2453
2657
|
<Update description={date} href={href} id={id} label={label} tags={tags}>
|
|
2454
|
-
<Content />
|
|
2658
|
+
<Content components={components} />
|
|
2455
2659
|
</Update>
|
|
2456
2660
|
))}
|
|
2457
2661
|
</div>
|
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
} from "../../deploy/function-bundle.ts";
|
|
46
46
|
import { platformRedirects } from "../../deploy/redirects.ts";
|
|
47
47
|
import { injectNegotiationRoutes } from "../../deploy/vercel-negotiation.ts";
|
|
48
|
+
import { cardCacheTally, ogCacheDir, pruneCardCache } from "../../og/cache.ts";
|
|
48
49
|
import { commandMeta } from "../command-meta.ts";
|
|
49
50
|
import { refuseIfDevRunning } from "../dev-lock.ts";
|
|
50
51
|
import { logger } from "../log.ts";
|
|
@@ -248,6 +249,29 @@ const emitCloudflareNegotiation = async (
|
|
|
248
249
|
);
|
|
249
250
|
};
|
|
250
251
|
|
|
252
|
+
/**
|
|
253
|
+
* Report how many OG cards the build read back from the on-disk cache against
|
|
254
|
+
* how many it rendered (the endpoint tallies both), so a warm rebuild shows
|
|
255
|
+
* where the time went. With `prune`, also drop the cached cards this build
|
|
256
|
+
* never asked for — renamed pages, edited descriptions, cards from an earlier
|
|
257
|
+
* Blume version — so a persisted cache holds exactly the current site's cards.
|
|
258
|
+
*/
|
|
259
|
+
const reportCardCache = async (
|
|
260
|
+
project: BlumeProject,
|
|
261
|
+
prune: boolean
|
|
262
|
+
): Promise<void> => {
|
|
263
|
+
const cards = cardCacheTally();
|
|
264
|
+
if (!cards) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
logger.info(
|
|
268
|
+
`OG cards: ${cards.hits} reused from the cache, ${cards.misses} rendered`
|
|
269
|
+
);
|
|
270
|
+
if (prune) {
|
|
271
|
+
await pruneCardCache(ogCacheDir(project.context));
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
251
275
|
const formatBytes = (bytes: number): string => {
|
|
252
276
|
if (bytes < 1024) {
|
|
253
277
|
return `${bytes} B`;
|
|
@@ -557,6 +581,10 @@ export const buildCommand = defineCommand({
|
|
|
557
581
|
root: project.context.outDir,
|
|
558
582
|
});
|
|
559
583
|
|
|
584
|
+
// A real build also prunes the cache; an isolated verify must not evict
|
|
585
|
+
// cards a live dev server is still serving.
|
|
586
|
+
await reportCardCache(project, !runtimeDir);
|
|
587
|
+
|
|
560
588
|
// The bundle report and budget gate still run for an isolated build —
|
|
561
589
|
// `blume build --isolated --budget-js 100` exiting 0 without measuring
|
|
562
590
|
// anything would be a silent false pass in CI.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { isImageIcon, isInlineSvg } from "../theme/icon-kind.ts";
|
|
3
3
|
import { resolveIcon } from "../theme/icons.ts";
|
|
4
|
+
import { iconSpriteFor, registerIconSymbol } from "./icon-sprite.ts";
|
|
4
5
|
import { withBase } from "./islands/base-path.ts";
|
|
5
6
|
|
|
6
7
|
interface Props {
|
|
@@ -32,6 +33,17 @@ const rawSvg = iconName && isInlineSvg(iconName) ? iconName.trim() : null;
|
|
|
32
33
|
const imageSrc = iconName && !rawSvg && isImageIcon(iconName) ? iconName : null;
|
|
33
34
|
const resolvedIcon =
|
|
34
35
|
iconName && !(imageSrc || rawSvg) ? resolveIcon(iconName) : null;
|
|
36
|
+
// A set icon joins the page's sprite when the shell keeps one (see
|
|
37
|
+
// icon-sprite.ts): the paths render once, in the sprite, and this use is a
|
|
38
|
+
// <use> reference. Without a sprite the icon inlines its paths as before.
|
|
39
|
+
const sprite = resolvedIcon ? iconSpriteFor(Astro.locals) : undefined;
|
|
40
|
+
const symbolId =
|
|
41
|
+
sprite && resolvedIcon
|
|
42
|
+
? registerIconSymbol(sprite, resolvedIcon.name, {
|
|
43
|
+
body: resolvedIcon.body,
|
|
44
|
+
viewBox: resolvedIcon.viewBox,
|
|
45
|
+
})
|
|
46
|
+
: null;
|
|
35
47
|
const resolvedClass = astroClass ?? className;
|
|
36
48
|
const customStyle = `display:inline-flex;width:${size}px;height:${size}px;${
|
|
37
49
|
color ? `color:${color}` : ""
|
|
@@ -68,6 +80,18 @@ const style = color ? `color:${color}` : undefined;
|
|
|
68
80
|
src={withBase(imageSrc)}
|
|
69
81
|
width={size}
|
|
70
82
|
/>
|
|
83
|
+
) : symbolId ? (
|
|
84
|
+
<svg
|
|
85
|
+
aria-hidden={label ? undefined : "true"}
|
|
86
|
+
aria-label={label}
|
|
87
|
+
class={resolvedClass}
|
|
88
|
+
height={size}
|
|
89
|
+
role={label ? "img" : undefined}
|
|
90
|
+
style={style}
|
|
91
|
+
width={size}
|
|
92
|
+
>
|
|
93
|
+
<use href={`#${symbolId}`} />
|
|
94
|
+
</svg>
|
|
71
95
|
) : (
|
|
72
96
|
resolvedIcon && (
|
|
73
97
|
<svg
|
|
@@ -20,7 +20,7 @@ const src = videoId ? youtubeEmbedUrl(videoId, { start }) : null;
|
|
|
20
20
|
|
|
21
21
|
{
|
|
22
22
|
src && (
|
|
23
|
-
<div class="not-prose my-6 aspect-video overflow-hidden rounded-blume border border-border bg-muted/30">
|
|
23
|
+
<div class="not-prose my-6 aspect-video w-full overflow-hidden rounded-blume border border-border bg-muted/30">
|
|
24
24
|
<iframe
|
|
25
25
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
|
26
26
|
allowfullscreen
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from "astro";
|
|
2
|
+
|
|
3
|
+
import { iconSpriteFor, renderIconSprite } from "./icon-sprite.ts";
|
|
4
|
+
import type { IconSprite } from "./icon-sprite.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The placeholder a shell renders where its icon sprite goes (the end of the
|
|
8
|
+
* body); {@link onRequest} replaces it once the page has fully rendered.
|
|
9
|
+
* Rendering the sprite from a component instead wouldn't work: Astro walks a
|
|
10
|
+
* template's own markup before its nested components render, so a sprite
|
|
11
|
+
* component at the end of the body runs before the header's search button or
|
|
12
|
+
* the page content's callouts have registered their icons.
|
|
13
|
+
*/
|
|
14
|
+
export const ICON_SPRITE_SLOT = "<!--blume-icon-sprite-->";
|
|
15
|
+
|
|
16
|
+
/** The page HTML with its placeholder replaced by the finished sprite. */
|
|
17
|
+
export const spliceIconSprite = (html: string, sprite?: IconSprite): string =>
|
|
18
|
+
html.includes(ICON_SPRITE_SLOT)
|
|
19
|
+
? html.replace(ICON_SPRITE_SLOT, sprite ? renderIconSprite(sprite) : "")
|
|
20
|
+
: html;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Blume's icon-sprite middleware (wired by the integration). Draining the
|
|
24
|
+
* response renders the whole page, after which the request's registry holds
|
|
25
|
+
* every icon it used; the sprite is spliced in where the shell left the slot.
|
|
26
|
+
* Requests whose shell kept no registry — a user layout override, a partial,
|
|
27
|
+
* an endpoint — pass through untouched, streaming and all.
|
|
28
|
+
*/
|
|
29
|
+
export const onRequest: MiddlewareHandler = async (context, render) => {
|
|
30
|
+
const response = await render();
|
|
31
|
+
const sprite = iconSpriteFor(context.locals);
|
|
32
|
+
if (!sprite) {
|
|
33
|
+
return response;
|
|
34
|
+
}
|
|
35
|
+
const html = await response.text();
|
|
36
|
+
return new Response(spliceIconSprite(html, sprite), {
|
|
37
|
+
headers: response.headers,
|
|
38
|
+
status: response.status,
|
|
39
|
+
statusText: response.statusText,
|
|
40
|
+
});
|
|
41
|
+
};
|