blume 1.5.2 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +85 -0
- package/dist/cli/index.js +3639 -1377
- package/dist/cli/index.js.map +103 -91
- package/dist/types/ai/component-markdown.d.ts +79 -0
- package/dist/types/components/layout/nav-utils.d.ts +60 -0
- package/dist/types/core/base-path.d.ts +9 -0
- package/dist/types/core/config-input.d.ts +206 -4
- package/dist/types/core/config.d.ts +6 -4
- package/dist/types/core/data.d.ts +23 -1
- package/dist/types/core/github.d.ts +35 -0
- package/dist/types/core/i18n-ui.d.ts +8 -0
- package/dist/types/core/navigation.d.ts +69 -0
- package/dist/types/core/schema.d.ts +117 -1
- package/dist/types/core/sources/types.d.ts +31 -6
- package/dist/types/core/types.d.ts +23 -2
- package/dist/types/markdown/features.d.ts +21 -0
- package/dist/types/openapi/references.d.ts +21 -1
- package/dist/types/seo/jsonld.d.ts +105 -0
- package/dist/types/theme/fonts.d.ts +34 -4
- package/docs/_snippets/include-demo.mdx +7 -0
- package/docs/advanced/api-reference.mdx +3 -3
- package/docs/advanced/custom-pages.mdx +1 -1
- package/docs/advanced/graphql.mdx +84 -0
- package/docs/advanced/meta.ts +8 -1
- package/docs/configuration/ai.mdx +21 -3
- package/docs/configuration/index.mdx +24 -0
- package/docs/configuration/search.mdx +13 -1
- package/docs/configuration/seo.mdx +27 -0
- package/docs/configuration/theming.mdx +17 -0
- package/docs/content/components.mdx +7 -0
- package/docs/content/includes.mdx +68 -0
- package/docs/content/meta.ts +1 -0
- package/docs/content/navigation.mdx +25 -0
- package/docs/content/sources.mdx +42 -1
- package/docs/content/syntax.mdx +69 -1
- package/docs/content/versioning.mdx +15 -9
- package/docs/reference/cli.mdx +2 -1
- package/package.json +23 -14
- package/skills/blume-migrate/SKILL.md +16 -7
- package/skills/blume-migrate/references/docusaurus.md +5 -3
- package/skills/blume-migrate/references/fumadocs.md +10 -2
- package/skills/blume-migrate/references/mintlify.md +3 -2
- package/skills/blume-migrate/references/nextra.md +2 -2
- package/skills/blume-migrate/references/starlight.md +1 -1
- package/src/ai/agent-readability.ts +2 -1
- package/src/ai/ask-data.ts +2 -1
- package/src/ai/component-markdown.ts +199 -36
- package/src/ai/llms.ts +93 -6
- package/src/ai/markdown.ts +2 -2
- package/src/ai/mcp/discovery.ts +10 -2
- package/src/ai/mcp/server.ts +74 -2
- package/src/astro/generate.ts +183 -116
- package/src/astro/include-hmr.ts +81 -0
- package/src/astro/include-refresh.ts +0 -0
- package/src/astro/index.ts +3 -5
- package/src/astro/templates.ts +125 -76
- package/src/cli/commands/build.ts +84 -15
- package/src/cli/init/questions.ts +1 -0
- package/src/cli/init/scaffold.ts +27 -4
- package/src/components/colors.ts +142 -0
- package/src/components/content/Badge.astro +5 -12
- package/src/components/content/Callout.astro +19 -36
- package/src/components/content/Card.astro +15 -21
- package/src/components/content/Component.astro +10 -1
- package/src/components/content/GithubInfo.astro +28 -9
- package/src/components/content/Tabs.astro +27 -5
- package/src/components/content/github-info.ts +20 -5
- package/src/components/dropdown-dismiss.ts +122 -0
- package/src/components/layout/Fonts.astro +15 -8
- package/src/components/layout/Header.astro +44 -0
- package/src/components/layout/LanguageSwitcher.astro +9 -1
- package/src/components/layout/NavSelector.astro +12 -3
- package/src/components/layout/NavTree.astro +6 -18
- package/src/components/layout/PageActions.astro +29 -8
- package/src/components/layout/PageLayout.astro +10 -1
- package/src/components/layout/ReferenceLayout.astro +6 -1
- package/src/components/layout/RootLayout.astro +46 -15
- package/src/components/layout/Search.astro +36 -4
- package/src/components/layout/TableOfContents.astro +8 -2
- package/src/components/layout/head-scripts.ts +53 -1
- package/src/components/openapi/ApiOverview.astro +13 -3
- package/src/components/openapi/AsyncApiOperation.astro +7 -14
- package/src/components/openapi/GraphqlChip.astro +33 -0
- package/src/components/openapi/GraphqlFieldsTable.astro +111 -0
- package/src/components/openapi/GraphqlOperation.astro +186 -0
- package/src/components/openapi/GraphqlType.astro +154 -0
- package/src/components/openapi/MethodBadge.astro +3 -14
- package/src/components/openapi/Operation.astro +12 -5
- package/src/components/openapi/OperationPanel.astro +43 -0
- package/src/components/openapi/RequestPanel.astro +5 -10
- package/src/components/openapi/Responses.astro +1 -16
- package/src/components/openapi/graphql-helpers.ts +466 -0
- package/src/components/openapi/playground-client.ts +15 -0
- package/src/components/openapi/sample-panels.ts +45 -0
- package/src/components/openapi/snippets.ts +13 -35
- package/src/core/base-path.ts +11 -0
- package/src/core/config-input.ts +209 -2
- package/src/core/config.ts +6 -4
- package/src/core/content-assets.ts +15 -4
- package/src/core/data.ts +18 -2
- package/src/core/diagnostics.ts +8 -0
- package/src/core/frontmatter.ts +20 -8
- package/src/core/github.ts +71 -0
- package/src/core/graph.ts +22 -8
- package/src/core/heading-markers.ts +96 -0
- package/src/core/i18n-ui.ts +11 -0
- package/src/core/includes.ts +632 -0
- package/src/core/last-modified.ts +36 -11
- package/src/core/links.ts +79 -13
- package/src/core/meta.ts +2 -1
- package/src/core/nav-diagnostics.ts +11 -2
- package/src/core/navigation.ts +27 -6
- package/src/core/project-graph.ts +61 -9
- package/src/core/schema.ts +226 -35
- package/src/core/server-features.ts +5 -9
- package/src/core/sources/github-releases.ts +2 -2
- package/src/core/sources/normalize.ts +502 -115
- package/src/core/sources/notion.ts +43 -8
- package/src/core/sources/obsidian.ts +1038 -0
- package/src/core/sources/read.ts +36 -1
- package/src/core/sources/resolve.ts +34 -1
- package/src/core/sources/types.ts +28 -6
- package/src/core/sources/watch.ts +12 -8
- package/src/core/tsconfig-aliases.ts +48 -35
- package/src/core/types.ts +25 -2
- package/src/core/ui-packs/ar.ts +1 -0
- package/src/core/ui-packs/bg.ts +2 -0
- package/src/core/ui-packs/bn.ts +1 -0
- package/src/core/ui-packs/ca.ts +2 -0
- package/src/core/ui-packs/cs.ts +1 -0
- package/src/core/ui-packs/da.ts +1 -0
- package/src/core/ui-packs/de.ts +2 -0
- package/src/core/ui-packs/el.ts +2 -0
- package/src/core/ui-packs/es.ts +2 -0
- package/src/core/ui-packs/fa.ts +1 -0
- package/src/core/ui-packs/fi.ts +1 -0
- package/src/core/ui-packs/fr.ts +2 -0
- package/src/core/ui-packs/he.ts +1 -0
- package/src/core/ui-packs/hi.ts +1 -0
- package/src/core/ui-packs/hr.ts +2 -0
- package/src/core/ui-packs/hu.ts +2 -0
- package/src/core/ui-packs/id.ts +2 -0
- package/src/core/ui-packs/it.ts +1 -0
- package/src/core/ui-packs/ja.ts +2 -0
- package/src/core/ui-packs/ko.ts +2 -0
- package/src/core/ui-packs/nl.ts +2 -0
- package/src/core/ui-packs/no.ts +2 -0
- package/src/core/ui-packs/pl.ts +2 -0
- package/src/core/ui-packs/pt-br.ts +2 -0
- package/src/core/ui-packs/pt.ts +2 -0
- package/src/core/ui-packs/ro.ts +2 -0
- package/src/core/ui-packs/ru.ts +2 -0
- package/src/core/ui-packs/sk.ts +1 -0
- package/src/core/ui-packs/sr.ts +1 -0
- package/src/core/ui-packs/sv.ts +2 -0
- package/src/core/ui-packs/th.ts +1 -0
- package/src/core/ui-packs/tr.ts +2 -0
- package/src/core/ui-packs/uk.ts +2 -0
- package/src/core/ui-packs/vi.ts +1 -0
- package/src/core/ui-packs/zh-tw.ts +1 -0
- package/src/core/ui-packs/zh.ts +1 -0
- package/src/core/version-cut.ts +21 -3
- package/src/core/yaml.ts +26 -0
- package/src/deploy/function-bundle.ts +251 -0
- package/src/eval/schema.ts +3 -1
- package/src/markdown/code-title.ts +22 -16
- package/src/markdown/features.ts +21 -0
- package/src/markdown/fence-meta.ts +50 -0
- package/src/markdown/heading-anchors.ts +198 -37
- package/src/markdown/include.ts +247 -0
- package/src/markdown/index.ts +43 -34
- package/src/markdown/language-icon.ts +2 -2
- package/src/markdown/mdast.ts +7 -3
- package/src/markdown/ts2js.ts +264 -0
- package/src/openapi/asyncapi.ts +4 -1
- package/src/openapi/graphql-build.ts +293 -0
- package/src/openapi/graphql.ts +212 -0
- package/src/openapi/model.ts +38 -5
- package/src/openapi/parse.ts +34 -0
- package/src/openapi/proxy.ts +30 -5
- package/src/openapi/references.ts +89 -13
- package/src/openapi/render-mdx.ts +48 -8
- package/src/openapi/scalar.ts +5 -12
- package/src/openapi/source.ts +91 -23
- package/src/registry/eject.ts +11 -0
- package/src/search/documents.ts +229 -37
- package/src/search/orama-index.ts +9 -5
- package/src/seo/jsonld.ts +293 -51
- package/src/theme/code-block-padding.ts +16 -0
- package/src/theme/entry.ts +65 -11
- package/src/theme/fonts.ts +189 -16
- package/src/translate/prompts.ts +2 -0
- package/src/translate/run.ts +7 -0
- package/src/translate/work-list.ts +0 -0
package/src/astro/templates.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { askBackendRuntimeDep } from "../ai/ask.ts";
|
|
|
8
8
|
import type { AskBackend } from "../ai/ask.ts";
|
|
9
9
|
import { buildHomeLinkHeader } from "../ai/link-headers.ts";
|
|
10
10
|
import { normalizeBasePath } from "../core/base-path.ts";
|
|
11
|
+
import { TOC_HIDDEN_KEY } from "../core/heading-markers.ts";
|
|
11
12
|
import type { ResolvedConfig } from "../core/schema.ts";
|
|
12
13
|
import { BLUME_IGNORE_DIRS } from "../core/sources/watch.ts";
|
|
13
14
|
import { trimChar } from "../core/trim.ts";
|
|
@@ -16,7 +17,7 @@ import { applyBaseToAstroRedirects } from "../deploy/redirects.ts";
|
|
|
16
17
|
import type { OgFont, OgFontFamilies } from "../og/card.ts";
|
|
17
18
|
import { hasScalarReferences } from "../openapi/references.ts";
|
|
18
19
|
import { searchProviderMeta } from "../search/providers.ts";
|
|
19
|
-
import { buildFontEntries } from "../theme/fonts.ts";
|
|
20
|
+
import { buildFontEntries, fontLocaleCodes } from "../theme/fonts.ts";
|
|
20
21
|
import type { ExampleSpec } from "./examples.ts";
|
|
21
22
|
import type { BlumePageRoute } from "./integration.ts";
|
|
22
23
|
import type { IslandSpec } from "./islands.ts";
|
|
@@ -307,35 +308,6 @@ const reactIntegration = (compilerPath: string | null | undefined): string =>
|
|
|
307
308
|
? `react({ babel: { plugins: [[${JSON.stringify(compilerPath)}, { target: "19" }]] }, ${REACT_EXCLUDE} })`
|
|
308
309
|
: `react({ ${REACT_EXCLUDE} })`;
|
|
309
310
|
|
|
310
|
-
/**
|
|
311
|
-
* The `server.watch` block for the generated dev config. Keeps the watcher out
|
|
312
|
-
* of Astro's cache dir — but ONLY when the docs collection is rooted at a
|
|
313
|
-
* directory containing the runtime dir (a migrated, `content.root: "."`
|
|
314
|
-
* project). There, the glob loader's watcher match (`picomatch.isMatch(entry,
|
|
315
|
-
* pattern)` with array-OR semantics, where any negated pattern matches
|
|
316
|
-
* unrelated files) fires on every `.blume/.astro` write — "No entry type
|
|
317
|
-
* found" noise, and a `data-store.json` event can re-ingest the store file as
|
|
318
|
-
* a JSON entry and loop the sync. Everywhere else the watcher MUST see
|
|
319
|
-
* `.astro/data-store.json`: its change events are the only trigger for
|
|
320
|
-
* Astro's dev-time content invalidation (see vite-plugin-content-virtual-mod),
|
|
321
|
-
* and `.md` bodies are rendered into the store at load time — so ignoring the
|
|
322
|
-
* file serves stale `.md` HTML on every request until the server restarts,
|
|
323
|
-
* even though the loader logs a reload.
|
|
324
|
-
*/
|
|
325
|
-
const devWatchOption = (
|
|
326
|
-
outDir: string,
|
|
327
|
-
contentWatchesRuntimeDir: boolean | undefined
|
|
328
|
-
): string =>
|
|
329
|
-
contentWatchesRuntimeDir
|
|
330
|
-
? `
|
|
331
|
-
// Astro's cache dir sits inside the docs collection, whose watcher would
|
|
332
|
-
// otherwise churn (and can loop) on Astro's own writes. Trade-off: .md
|
|
333
|
-
// body edits need a dev-server restart in this layout.
|
|
334
|
-
watch: {
|
|
335
|
-
ignored: ${JSON.stringify([join(outDir, ".astro", "**")])},
|
|
336
|
-
},`
|
|
337
|
-
: "";
|
|
338
|
-
|
|
339
311
|
interface IntegrationBridgeOptions {
|
|
340
312
|
/** Config path relative to the generated Astro config. */
|
|
341
313
|
configFile: string;
|
|
@@ -387,8 +359,11 @@ interface OptimizeDepsConfig {
|
|
|
387
359
|
* the Vite root is the generated runtime, so user pages, convention islands,
|
|
388
360
|
* and alias-reachable components all live outside it and are otherwise only
|
|
389
361
|
* crawled when first requested. The compiler runtime rides the include list
|
|
390
|
-
* because it is Babel-injected and no source scan can see it.
|
|
391
|
-
*
|
|
362
|
+
* because it is Babel-injected and no source scan can see it. @vitejs/plugin-react
|
|
363
|
+
* would add it itself, but only when the babel plugin is passed by its bare
|
|
364
|
+
* name (`getReactCompilerPlugin` is an exact string match) — Blume passes an
|
|
365
|
+
* absolute path (see `reactIntegration`), which that check never matches. See
|
|
366
|
+
* the optimizeDeps comment in the generated config for the failure this prevents.
|
|
392
367
|
*/
|
|
393
368
|
const resolveOptimizeDeps = (options: {
|
|
394
369
|
aliases: Record<string, string> | undefined;
|
|
@@ -445,12 +420,10 @@ export const astroConfigTemplate = (options: {
|
|
|
445
420
|
/** Project tsconfig path aliases (`find` -> absolute dir), e.g. `@` -> src. */
|
|
446
421
|
aliases?: Record<string, string>;
|
|
447
422
|
/**
|
|
448
|
-
*
|
|
449
|
-
*
|
|
450
|
-
* only layout where the dev watcher must be kept out of Astro's cache dir.
|
|
451
|
-
* See {@link devWatchOption} for why this must stay scoped.
|
|
423
|
+
* The docs collection's content root; bounds `<include>` resolution in the
|
|
424
|
+
* processors and locates the include graph for dev-server invalidation.
|
|
452
425
|
*/
|
|
453
|
-
|
|
426
|
+
contentRoot?: string;
|
|
454
427
|
/** Bridge used to load configured integrations without serializing them. */
|
|
455
428
|
integrationBridge?: IntegrationBridgeOptions;
|
|
456
429
|
}): string => {
|
|
@@ -552,7 +525,12 @@ export const astroConfigTemplate = (options: {
|
|
|
552
525
|
// `fontProviders` is only imported when at least one font is configured.
|
|
553
526
|
// Local variant sources are emitted as absolute paths (the Astro root is
|
|
554
527
|
// `.blume/`, not the user's project, so root-relative paths would miss).
|
|
555
|
-
|
|
528
|
+
// Subsets follow the configured locales (a Vietnamese site loads the
|
|
529
|
+
// `vietnamese` faces) unless a family pins its own.
|
|
530
|
+
const fontEntries = buildFontEntries(
|
|
531
|
+
config.theme.fonts,
|
|
532
|
+
fontLocaleCodes(config.i18n)
|
|
533
|
+
);
|
|
556
534
|
const fontsOption = fontEntries.length
|
|
557
535
|
? `\n fonts: [${fontEntries
|
|
558
536
|
.map((font) =>
|
|
@@ -588,6 +566,8 @@ export const astroConfigTemplate = (options: {
|
|
|
588
566
|
font.cssVariable
|
|
589
567
|
)}, weights: ${JSON.stringify(
|
|
590
568
|
font.weights
|
|
569
|
+
)}, subsets: ${JSON.stringify(
|
|
570
|
+
font.subsets
|
|
591
571
|
)}, fallbacks: ${JSON.stringify(font.fallbacks)} }`
|
|
592
572
|
)
|
|
593
573
|
.join(", ")}],`
|
|
@@ -606,8 +586,8 @@ export const astroConfigTemplate = (options: {
|
|
|
606
586
|
: "";
|
|
607
587
|
const blumeImports = [
|
|
608
588
|
"blumeIntegration",
|
|
589
|
+
"includeHmrPlugin",
|
|
609
590
|
"prerenderDepsPlugin",
|
|
610
|
-
"serverAppResolvePlugin",
|
|
611
591
|
...(adapterOption.includes("withAdapterRoot") ? ["withAdapterRoot"] : []),
|
|
612
592
|
];
|
|
613
593
|
const blumeImport = `import { ${blumeImports.join(", ")} } from "blume/astro";\n`;
|
|
@@ -632,6 +612,7 @@ export const astroConfigTemplate = (options: {
|
|
|
632
612
|
`mdx({ processor: blumeMdxProcessor(${JSON.stringify({
|
|
633
613
|
basePath: config.basePath,
|
|
634
614
|
codeThemes: config.markdown.codeBlocks.theme,
|
|
615
|
+
contentRoot: options.contentRoot,
|
|
635
616
|
deployBase,
|
|
636
617
|
headingAnchors: config.markdown.headingAnchors,
|
|
637
618
|
})}) })`,
|
|
@@ -657,10 +638,6 @@ export const astroConfigTemplate = (options: {
|
|
|
657
638
|
})})`
|
|
658
639
|
);
|
|
659
640
|
|
|
660
|
-
const watchOption = devWatchOption(
|
|
661
|
-
context.outDir,
|
|
662
|
-
options.contentWatchesRuntimeDir
|
|
663
|
-
);
|
|
664
641
|
const {
|
|
665
642
|
configSourceMarker,
|
|
666
643
|
userConfigImports,
|
|
@@ -685,6 +662,7 @@ ${userConfigSetup}export default defineConfig({
|
|
|
685
662
|
processor: blumeMarkdownProcessor(${JSON.stringify({
|
|
686
663
|
basePath: config.basePath,
|
|
687
664
|
codeThemes: config.markdown.codeBlocks.theme,
|
|
665
|
+
contentRoot: options.contentRoot,
|
|
688
666
|
deployBase,
|
|
689
667
|
headingAnchors: config.markdown.headingAnchors,
|
|
690
668
|
})}),
|
|
@@ -706,7 +684,9 @@ ${userConfigSetup}export default defineConfig({
|
|
|
706
684
|
// instantly.
|
|
707
685
|
prefetch: { prefetchAll: true },
|
|
708
686
|
vite: {
|
|
709
|
-
plugins: [tailwindcss(),
|
|
687
|
+
plugins: [tailwindcss(), includeHmrPlugin(${JSON.stringify(
|
|
688
|
+
`${context.outDir}/src/generated/includes.json`
|
|
689
|
+
)}), prerenderDepsPlugin()],
|
|
710
690
|
// Everything hydration can reach must be part of the dev dep optimizer's
|
|
711
691
|
// FIRST run. The Vite root is the generated runtime, so user pages,
|
|
712
692
|
// islands, and aliased components live outside it and are only crawled
|
|
@@ -764,7 +744,7 @@ ${userConfigSetup}export default defineConfig({
|
|
|
764
744
|
server: {
|
|
765
745
|
fs: {
|
|
766
746
|
allow: ${JSON.stringify(fsAllow)},
|
|
767
|
-
}
|
|
747
|
+
},
|
|
768
748
|
},
|
|
769
749
|
},
|
|
770
750
|
});
|
|
@@ -778,14 +758,11 @@ export const stagedContentDir = (outDir: string): string =>
|
|
|
778
758
|
/**
|
|
779
759
|
* The runtime dir relative to the docs collection `base` when it sits inside
|
|
780
760
|
* it (a migrated, `content.root: "."` project) — null when it lives elsewhere.
|
|
781
|
-
* Drives
|
|
782
|
-
*
|
|
783
|
-
*
|
|
761
|
+
* Drives the collection's negative glob in `contentConfigTemplate`, which both
|
|
762
|
+
* keeps runtime-dir files out of the collection and (Astro's watcher honors
|
|
763
|
+
* negated patterns) keeps the content watcher off Astro's own `.astro` writes.
|
|
784
764
|
*/
|
|
785
|
-
|
|
786
|
-
base: string,
|
|
787
|
-
outDir: string
|
|
788
|
-
): string | null => {
|
|
765
|
+
const runtimeDirWithin = (base: string, outDir: string): string | null => {
|
|
789
766
|
const rel = relative(base, outDir);
|
|
790
767
|
return rel && !rel.startsWith("..") && !isAbsolute(rel) ? rel : null;
|
|
791
768
|
};
|
|
@@ -836,15 +813,11 @@ export const contentConfigTemplate = (options: {
|
|
|
836
813
|
const outDirRel = runtimeDirWithin(collectionBase, context.outDir);
|
|
837
814
|
const outDirIgnore = outDirRel ? [`!${outDirRel}/**`] : [];
|
|
838
815
|
|
|
839
|
-
// With no filesystem source, no route renders through `docs`, so glob
|
|
840
|
-
//
|
|
841
|
-
//
|
|
842
|
-
// collection
|
|
843
|
-
//
|
|
844
|
-
// — with array-OR semantics, any `!ignored/**` negation *matches* unrelated
|
|
845
|
-
// files, so negative patterns can't exclude a subtree there. An empty pattern
|
|
846
|
-
// matches nothing, so the watcher stays silent. The collection is still
|
|
847
|
-
// declared below so `getCollection("docs")` / `getEntry` resolve (to empty).
|
|
816
|
+
// With no filesystem source, no route renders through `docs`, so glob
|
|
817
|
+
// nothing: an all-staged project roots the collection at the project dir,
|
|
818
|
+
// and a patterned glob would scan (and watch) the whole project for nothing.
|
|
819
|
+
// The collection is still declared below so `getCollection("docs")` /
|
|
820
|
+
// `getEntry` resolve (to empty).
|
|
848
821
|
const filesystem = options.filesystem ?? true;
|
|
849
822
|
const docsPattern = filesystem
|
|
850
823
|
? [
|
|
@@ -880,13 +853,17 @@ const staged = defineCollection({
|
|
|
880
853
|
return `// Generated by Blume. Do not edit.
|
|
881
854
|
import { defineCollection } from "astro:content";
|
|
882
855
|
import { glob } from "astro/loaders";
|
|
856
|
+
import { withIncludeRefresh } from "blume/astro";
|
|
883
857
|
|
|
858
|
+
// withIncludeRefresh keeps <include>-bearing pages fresh: plain .md entries
|
|
859
|
+
// are rendered at sync time and digest-cached on the page file alone, so a
|
|
860
|
+
// partial edit (or a warm-cache rebuild after one) would serve stale HTML.
|
|
884
861
|
const docs = defineCollection({
|
|
885
|
-
loader: glob({
|
|
862
|
+
loader: withIncludeRefresh(glob({
|
|
886
863
|
pattern: ${JSON.stringify(docsPattern)},
|
|
887
864
|
base: ${JSON.stringify(astroGlobBase(collectionBase))},
|
|
888
865
|
generateId: ({ entry }) => entry,
|
|
889
|
-
}),
|
|
866
|
+
}), ${JSON.stringify(`${context.outDir}/src/generated/includes.json`)}),
|
|
890
867
|
});
|
|
891
868
|
${stagedBlock}
|
|
892
869
|
export const collections = { docs${options.staged ? ", staged" : ""} };
|
|
@@ -1560,10 +1537,11 @@ export function getStaticPaths() {
|
|
|
1560
1537
|
return paths;
|
|
1561
1538
|
}
|
|
1562
1539
|
|
|
1563
|
-
// Footer branding shared by every card. The
|
|
1564
|
-
//
|
|
1565
|
-
|
|
1566
|
-
|
|
1540
|
+
// Footer branding shared by every card. The slug comes from the configured
|
|
1541
|
+
// repo rather than the URL, so an Enterprise host reads the same as github.com;
|
|
1542
|
+
// the site text (host plus deployment base) is resolved at generate time.
|
|
1543
|
+
const repoSlug = data.config.github
|
|
1544
|
+
? \`\${data.config.github.owner}/\${data.config.github.repo}\`
|
|
1567
1545
|
: undefined;
|
|
1568
1546
|
|
|
1569
1547
|
export async function GET({ props }: { props: { title: string } }) {
|
|
@@ -1786,7 +1764,18 @@ const entry = await getEntry(collection as CollectionKey, entryId);
|
|
|
1786
1764
|
if (!entry) {
|
|
1787
1765
|
return new Response(null, { status: 404 });
|
|
1788
1766
|
}
|
|
1789
|
-
const { Content, headings } = await render(entry);
|
|
1767
|
+
const { Content, headings: allHeadings, remarkPluginFrontmatter } = await render(entry);
|
|
1768
|
+
// \`[!toc]\`-marked headings render on the page but stay out of the table of
|
|
1769
|
+
// contents; the heading plugin reports their slugs through the render's
|
|
1770
|
+
// frontmatter (see markdown/heading-anchors.ts). Only the plugin's array
|
|
1771
|
+
// counts: \`frontmatter.extend\` can declare the same key, and on a page with
|
|
1772
|
+
// no headings that user-supplied value would pass straight through.
|
|
1773
|
+
const tocHiddenRaw = remarkPluginFrontmatter?.${TOC_HIDDEN_KEY};
|
|
1774
|
+
const tocHidden = new Set(Array.isArray(tocHiddenRaw) ? tocHiddenRaw : []);
|
|
1775
|
+
const headings =
|
|
1776
|
+
tocHidden.size > 0
|
|
1777
|
+
? allHeadings.filter((heading) => !tocHidden.has(heading.slug))
|
|
1778
|
+
: allHeadings;
|
|
1790
1779
|
const frontmatter = entry.data ?? {};
|
|
1791
1780
|
|
|
1792
1781
|
const seo = frontmatter.seo ?? {};
|
|
@@ -2312,6 +2301,7 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
|
|
|
2312
2301
|
*/
|
|
2313
2302
|
export const notFoundPageTemplate = (): string => `---
|
|
2314
2303
|
// Generated by Blume. Do not edit. Override by adding \`pages/404.astro\`.
|
|
2304
|
+
import Icon from "blume/components/Icon.astro";
|
|
2315
2305
|
import PageLayout from "blume/components/layout/PageLayout.astro";
|
|
2316
2306
|
import { withBase } from "blume/components/islands/base-path.ts";
|
|
2317
2307
|
import data from "blume:data";
|
|
@@ -2329,6 +2319,24 @@ const localeMeta = i18n
|
|
|
2329
2319
|
: null;
|
|
2330
2320
|
const dir = localeMeta?.dir ?? "ltr";
|
|
2331
2321
|
const htmlLang = i18n ? i18n.defaultLocale : "en";
|
|
2322
|
+
|
|
2323
|
+
// Recovery links, so a reader — or an agent that followed a stale URL — can
|
|
2324
|
+
// get back on track without guessing: every top-level section, then the
|
|
2325
|
+
// machine-readable indexes the build emits (the sitemap only exists with a
|
|
2326
|
+
// \`deployment.site\`; llms.txt only when \`ai.llmsTxt\` is on). Tabs link to
|
|
2327
|
+
// their resolved target when the section has no index page of its own.
|
|
2328
|
+
const suggestions = [
|
|
2329
|
+
...data.navigation.tabs.map((tab) => ({
|
|
2330
|
+
href: withBase(tab.href ?? tab.path),
|
|
2331
|
+
label: tab.label,
|
|
2332
|
+
})),
|
|
2333
|
+
...(data.config.discovery.sitemap
|
|
2334
|
+
? [{ href: withBase("/sitemap.xml"), label: nf.sitemap }]
|
|
2335
|
+
: []),
|
|
2336
|
+
...(data.config.discovery.llmsTxt
|
|
2337
|
+
? [{ href: withBase("/llms.txt"), label: nf.llms }]
|
|
2338
|
+
: []),
|
|
2339
|
+
];
|
|
2332
2340
|
---
|
|
2333
2341
|
|
|
2334
2342
|
<PageLayout
|
|
@@ -2349,15 +2357,56 @@ const htmlLang = i18n ? i18n.defaultLocale : "en";
|
|
|
2349
2357
|
noindex={true}
|
|
2350
2358
|
>
|
|
2351
2359
|
<div
|
|
2352
|
-
class="mx-auto
|
|
2360
|
+
class="mx-auto grid w-full max-w-5xl gap-12 px-6 py-20 sm:py-28 md:grid-cols-[3fr_2fr] md:gap-16 lg:gap-24 lg:py-36"
|
|
2353
2361
|
>
|
|
2354
|
-
<
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2362
|
+
<div class="flex flex-col items-start">
|
|
2363
|
+
<p
|
|
2364
|
+
class="font-mono text-xs font-medium tracking-widest text-muted-foreground"
|
|
2365
|
+
>
|
|
2366
|
+
404
|
|
2367
|
+
</p>
|
|
2368
|
+
<h1
|
|
2369
|
+
class="mt-4 text-balance text-4xl font-semibold tracking-tight text-foreground sm:text-5xl"
|
|
2370
|
+
>
|
|
2371
|
+
{nf.title}
|
|
2372
|
+
</h1>
|
|
2373
|
+
<p class="mt-4 max-w-md text-pretty text-lg text-muted-foreground">
|
|
2374
|
+
{nf.description}
|
|
2375
|
+
</p>
|
|
2376
|
+
<a
|
|
2377
|
+
class="mt-8 inline-flex items-center gap-1.5 rounded-full bg-accent py-2 pe-4 ps-3.5 text-sm font-medium text-accent-foreground transition-opacity hover:opacity-90"
|
|
2378
|
+
href={withBase("/")}
|
|
2379
|
+
>
|
|
2380
|
+
<Icon class="rtl:-scale-x-100" name="arrow-left" size={14} />
|
|
2381
|
+
{nf.home}
|
|
2382
|
+
</a>
|
|
2383
|
+
</div>
|
|
2384
|
+
{
|
|
2385
|
+
suggestions.length > 0 && (
|
|
2386
|
+
<nav aria-label={nf.suggestions} class="md:border-s md:border-border md:ps-12 lg:ps-16">
|
|
2387
|
+
<h2 class="text-xs font-medium uppercase tracking-widest text-muted-foreground">
|
|
2388
|
+
{nf.suggestions}
|
|
2389
|
+
</h2>
|
|
2390
|
+
<ul class="mt-4 divide-y divide-border border-y border-border">
|
|
2391
|
+
{suggestions.map((link) => (
|
|
2392
|
+
<li>
|
|
2393
|
+
<a
|
|
2394
|
+
class="group flex items-center justify-between gap-4 py-3 text-sm font-medium text-foreground transition-colors hover:text-accent"
|
|
2395
|
+
href={link.href}
|
|
2396
|
+
>
|
|
2397
|
+
<span>{link.label}</span>
|
|
2398
|
+
<Icon
|
|
2399
|
+
class="shrink-0 text-muted-foreground transition-transform group-hover:translate-x-0.5 rtl:-scale-x-100 rtl:group-hover:-translate-x-0.5"
|
|
2400
|
+
name="arrow-right"
|
|
2401
|
+
size={14}
|
|
2402
|
+
/>
|
|
2403
|
+
</a>
|
|
2404
|
+
</li>
|
|
2405
|
+
))}
|
|
2406
|
+
</ul>
|
|
2407
|
+
</nav>
|
|
2408
|
+
)
|
|
2409
|
+
}
|
|
2361
2410
|
</div>
|
|
2362
2411
|
</PageLayout>
|
|
2363
2412
|
`;
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
buildSkillsIndex,
|
|
26
26
|
collectSkills,
|
|
27
27
|
} from "../../ai/skills.ts";
|
|
28
|
+
import type { SkillArtifact } from "../../ai/skills.ts";
|
|
28
29
|
import {
|
|
29
30
|
buildSignaturesDirectory,
|
|
30
31
|
SIGNATURES_DIRECTORY_PATH,
|
|
@@ -46,6 +47,11 @@ import {
|
|
|
46
47
|
injectWorkerNegotiation,
|
|
47
48
|
NEGOTIATION_WORKER_FILE,
|
|
48
49
|
} from "../../deploy/cloudflare-negotiation.ts";
|
|
50
|
+
import {
|
|
51
|
+
auditVercelFunctions,
|
|
52
|
+
blumeDependencyNames,
|
|
53
|
+
functionBundleVerdict,
|
|
54
|
+
} from "../../deploy/function-bundle.ts";
|
|
49
55
|
import { buildNetlifyHeaders } from "../../deploy/headers.ts";
|
|
50
56
|
import {
|
|
51
57
|
buildNetlifyRedirects,
|
|
@@ -195,29 +201,29 @@ export const emitHeaderFiles = async (
|
|
|
195
201
|
};
|
|
196
202
|
|
|
197
203
|
/**
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
204
|
+
* Collect the Agent Skills `ai.skills` publishes, once per build, so both the
|
|
205
|
+
* skills surface and llms.txt (which lists them) read the same set. Empty
|
|
206
|
+
* when the feature is off, the directory is missing, nothing in it is
|
|
207
|
+
* publishable (each with a warning), or a user-shipped
|
|
208
|
+
* `public/.well-known/agent-skills/index.json` already owns the surface.
|
|
202
209
|
*/
|
|
203
|
-
const
|
|
210
|
+
const collectConfiguredSkills = async (
|
|
204
211
|
project: BlumeProject,
|
|
205
212
|
distDir: string
|
|
206
|
-
): Promise<
|
|
213
|
+
): Promise<SkillArtifact[]> => {
|
|
207
214
|
const configured = project.config.ai.skills;
|
|
208
215
|
if (!configured) {
|
|
209
|
-
return;
|
|
216
|
+
return [];
|
|
210
217
|
}
|
|
211
218
|
const dir = resolve(project.context.root, configured);
|
|
212
219
|
if (!existsSync(dir)) {
|
|
213
220
|
logger.warn(
|
|
214
221
|
`ai.skills points at "${configured}" (${dir}), which does not exist; no skills published.`
|
|
215
222
|
);
|
|
216
|
-
return;
|
|
223
|
+
return [];
|
|
217
224
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
return;
|
|
225
|
+
if (existsSync(join(distDir, AGENT_SKILLS_DIR.slice(1), "index.json"))) {
|
|
226
|
+
return [];
|
|
221
227
|
}
|
|
222
228
|
const { skills, warnings } = await collectSkills(dir);
|
|
223
229
|
for (const warning of warnings) {
|
|
@@ -225,8 +231,26 @@ const emitAgentSkills = async (
|
|
|
225
231
|
}
|
|
226
232
|
if (skills.length === 0) {
|
|
227
233
|
logger.warn(`ai.skills: no publishable skills found in "${configured}".`);
|
|
234
|
+
}
|
|
235
|
+
return skills;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Publish the collected Agent Skills: copy each skill artifact under
|
|
240
|
+
* `.well-known/agent-skills/` and emit the discovery index. A user-shipped
|
|
241
|
+
* `public/.well-known/agent-skills/index.json` takes over the whole surface
|
|
242
|
+
* (the collector returns nothing then), matching every other generated
|
|
243
|
+
* artifact.
|
|
244
|
+
*/
|
|
245
|
+
const emitAgentSkills = async (
|
|
246
|
+
project: BlumeProject,
|
|
247
|
+
distDir: string,
|
|
248
|
+
skills: readonly SkillArtifact[]
|
|
249
|
+
): Promise<void> => {
|
|
250
|
+
if (skills.length === 0) {
|
|
228
251
|
return;
|
|
229
252
|
}
|
|
253
|
+
const outDir = join(distDir, AGENT_SKILLS_DIR.slice(1));
|
|
230
254
|
await Promise.all(
|
|
231
255
|
skills.map(async (skill) => {
|
|
232
256
|
const target = join(outDir, skill.path);
|
|
@@ -329,6 +353,38 @@ const emitVercelNegotiation = async (
|
|
|
329
353
|
);
|
|
330
354
|
};
|
|
331
355
|
|
|
356
|
+
/**
|
|
357
|
+
* Refuse to ship a Vercel function bundle that would crash at runtime: a bare
|
|
358
|
+
* import the adapter's dependency trace silently dropped (see
|
|
359
|
+
* `deploy/function-bundle.ts`). A missing package that is one of Blume's own
|
|
360
|
+
* dependencies is fatal — the generated runtime imports it, so every request
|
|
361
|
+
* would die; a project's own external import is reported as a warning and left
|
|
362
|
+
* to the author.
|
|
363
|
+
*/
|
|
364
|
+
const checkVercelFunctionBundles = async (
|
|
365
|
+
outputDir: string,
|
|
366
|
+
root: string
|
|
367
|
+
): Promise<void> => {
|
|
368
|
+
const audits = await auditVercelFunctions(outputDir);
|
|
369
|
+
if (audits.length === 0) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const own = blumeDependencyNames();
|
|
373
|
+
let fatal = false;
|
|
374
|
+
for (const audit of audits) {
|
|
375
|
+
const verdict = functionBundleVerdict(audit, root, own);
|
|
376
|
+
if (verdict.fatal) {
|
|
377
|
+
fatal = true;
|
|
378
|
+
logger.error(verdict.message);
|
|
379
|
+
} else {
|
|
380
|
+
logger.warn(verdict.message);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (fatal) {
|
|
384
|
+
process.exit(1);
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
|
|
332
388
|
const warnCloudflareNegotiationSkipped = (): void =>
|
|
333
389
|
logger.warn(
|
|
334
390
|
"Could not wire Accept: text/markdown negotiation into dist/server/wrangler.json — raw Markdown stays available at the .md URLs."
|
|
@@ -559,7 +615,8 @@ export const isolatedStaticDir = (
|
|
|
559
615
|
*/
|
|
560
616
|
const publishLlmsFiles = async (
|
|
561
617
|
project: BlumeProject,
|
|
562
|
-
distDir: string
|
|
618
|
+
distDir: string,
|
|
619
|
+
skills: readonly SkillArtifact[]
|
|
563
620
|
): Promise<void> => {
|
|
564
621
|
const indexPath = join(distDir, "llms.txt");
|
|
565
622
|
const fullPath = join(distDir, "llms-full.txt");
|
|
@@ -568,7 +625,7 @@ const publishLlmsFiles = async (
|
|
|
568
625
|
if (!(writeIndex || writeFull)) {
|
|
569
626
|
return;
|
|
570
627
|
}
|
|
571
|
-
const { index, full } = await buildLlmsFiles(project);
|
|
628
|
+
const { index, full } = await buildLlmsFiles(project, { skills });
|
|
572
629
|
const writes: Promise<void>[] = [];
|
|
573
630
|
if (writeIndex) {
|
|
574
631
|
writes.push(writeFile(indexPath, index, "utf-8"));
|
|
@@ -613,8 +670,10 @@ const publishBuildArtifacts = async (
|
|
|
613
670
|
warn: (message) => logger.warn(message),
|
|
614
671
|
});
|
|
615
672
|
|
|
673
|
+
// Collected once: llms.txt lists the skills the build publishes below.
|
|
674
|
+
const skills = await collectConfiguredSkills(project, distDir);
|
|
616
675
|
if (project.config.ai.llmsTxt.enabled) {
|
|
617
|
-
await publishLlmsFiles(project, distDir);
|
|
676
|
+
await publishLlmsFiles(project, distDir, skills);
|
|
618
677
|
}
|
|
619
678
|
|
|
620
679
|
// A user's own public/ file (copied into dist by Astro) always wins.
|
|
@@ -652,7 +711,7 @@ const publishBuildArtifacts = async (
|
|
|
652
711
|
}
|
|
653
712
|
|
|
654
713
|
await emitWellKnownFiles(project.config, distDir);
|
|
655
|
-
await emitAgentSkills(project, distDir);
|
|
714
|
+
await emitAgentSkills(project, distDir, skills);
|
|
656
715
|
|
|
657
716
|
await emitRedirectFiles(project.config, distDir);
|
|
658
717
|
await emitHeaderFiles(project, distDir);
|
|
@@ -798,6 +857,15 @@ export const buildCommand = defineCommand({
|
|
|
798
857
|
// 100` exiting 0 without measuring anything would be a silent false pass
|
|
799
858
|
// in CI.
|
|
800
859
|
if (runtimeDir) {
|
|
860
|
+
if (
|
|
861
|
+
project.config.deployment.output === "server" &&
|
|
862
|
+
project.config.deployment.adapter === "vercel"
|
|
863
|
+
) {
|
|
864
|
+
await checkVercelFunctionBundles(
|
|
865
|
+
isolatedOutputDir(project.config, project.context),
|
|
866
|
+
root
|
|
867
|
+
);
|
|
868
|
+
}
|
|
801
869
|
await runClientAssetChecks(
|
|
802
870
|
isolatedStaticDir(project.config, project.context),
|
|
803
871
|
args
|
|
@@ -831,6 +899,7 @@ export const buildCommand = defineCommand({
|
|
|
831
899
|
}
|
|
832
900
|
|
|
833
901
|
if (project.config.deployment.output === "server" && adapter === "vercel") {
|
|
902
|
+
await checkVercelFunctionBundles(join(root, ".vercel", "output"), root);
|
|
834
903
|
await emitVercelNegotiation(project, markdownRoutePaths(project), root);
|
|
835
904
|
}
|
|
836
905
|
|
|
@@ -111,6 +111,7 @@ export const collectAnswers = async (
|
|
|
111
111
|
message: "Where does your content live?",
|
|
112
112
|
options: [
|
|
113
113
|
{ hint: "Local .mdx files", label: "filesystem", value: "filesystem" },
|
|
114
|
+
{ hint: "An Obsidian vault", label: "obsidian", value: "obsidian" },
|
|
114
115
|
{
|
|
115
116
|
hint: "Changelog from GitHub Releases",
|
|
116
117
|
label: "github-releases",
|
package/src/cli/init/scaffold.ts
CHANGED
|
@@ -15,6 +15,7 @@ export type PackageManager = (typeof PACKAGE_MANAGERS)[number];
|
|
|
15
15
|
/** The content-source kinds `init` can scaffold a config block for. */
|
|
16
16
|
export const SOURCE_KINDS = [
|
|
17
17
|
"filesystem",
|
|
18
|
+
"obsidian",
|
|
18
19
|
"github-releases",
|
|
19
20
|
"notion",
|
|
20
21
|
"sanity",
|
|
@@ -209,8 +210,12 @@ export const titleize = (raw: string): string => {
|
|
|
209
210
|
.join(" ");
|
|
210
211
|
};
|
|
211
212
|
|
|
212
|
-
/**
|
|
213
|
-
|
|
213
|
+
/**
|
|
214
|
+
* True when any selected source needs an explicit `content.sources` array —
|
|
215
|
+
* everything except `filesystem`, which is what the implicit default already
|
|
216
|
+
* desugars to.
|
|
217
|
+
*/
|
|
218
|
+
const needsExplicitSources = (sources: SourceKind[]): boolean =>
|
|
214
219
|
sources.some((source) => source !== "filesystem");
|
|
215
220
|
|
|
216
221
|
/**
|
|
@@ -239,6 +244,14 @@ const SOURCE_SNIPPETS = {
|
|
|
239
244
|
database: "your-database-id",
|
|
240
245
|
prefix: "notion",
|
|
241
246
|
},`,
|
|
247
|
+
obsidian: ` // An Obsidian vault, read in place. No export step, and no
|
|
248
|
+
// generated notes in your repo. Point \`vault\` at your vault directory,
|
|
249
|
+
// relative to this config file.
|
|
250
|
+
{
|
|
251
|
+
type: "obsidian",
|
|
252
|
+
vault: "vault",
|
|
253
|
+
prefix: "notes",
|
|
254
|
+
},`,
|
|
242
255
|
sanity: ` // Documents from a Sanity dataset. Private datasets read SANITY_TOKEN
|
|
243
256
|
// from the environment.
|
|
244
257
|
{
|
|
@@ -258,7 +271,7 @@ const SOURCE_SNIPPETS = {
|
|
|
258
271
|
const contentBlockFor = (answers: InitAnswers): string => {
|
|
259
272
|
const sources =
|
|
260
273
|
answers.sources.length === 0 ? ["filesystem" as const] : answers.sources;
|
|
261
|
-
if (!
|
|
274
|
+
if (!needsExplicitSources(sources)) {
|
|
262
275
|
return answers.contentDir === "docs"
|
|
263
276
|
? ""
|
|
264
277
|
: `
|
|
@@ -267,7 +280,7 @@ const contentBlockFor = (answers: InitAnswers): string => {
|
|
|
267
280
|
},`;
|
|
268
281
|
}
|
|
269
282
|
// Explicit sources replace the implicit filesystem desugar, so the local
|
|
270
|
-
// content dir must be listed alongside the
|
|
283
|
+
// content dir must be listed alongside the other sources to stay included.
|
|
271
284
|
const entries = SOURCE_KINDS.filter((kind) => sources.includes(kind)).map(
|
|
272
285
|
(kind) =>
|
|
273
286
|
kind === "filesystem"
|
|
@@ -328,6 +341,16 @@ export const buildPlan = (
|
|
|
328
341
|
.map((file) => ({ ...file, path: join(root, file.path) }))
|
|
329
342
|
);
|
|
330
343
|
}
|
|
344
|
+
// The obsidian snippet points at `vault/`; seed the directory with a first
|
|
345
|
+
// note so the scaffolded project passes the source's `validate()` and boots
|
|
346
|
+
// before the user has opened Obsidian at all.
|
|
347
|
+
if (answers.sources.includes("obsidian")) {
|
|
348
|
+
files.push({
|
|
349
|
+
content:
|
|
350
|
+
"# Welcome\n\nThis folder is read by Blume's `obsidian` source. Open it as a vault in Obsidian and write notes — `[[Wikilinks]]` become site links.\n",
|
|
351
|
+
path: join(root, "vault", "Welcome.md"),
|
|
352
|
+
});
|
|
353
|
+
}
|
|
331
354
|
return files;
|
|
332
355
|
};
|
|
333
356
|
|