blume 0.0.0 → 0.1.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/bin/blume.mjs +18 -0
- package/dist/cli/index.js +12696 -0
- package/dist/cli/index.js.map +145 -0
- package/dist/types/core/bridge.d.ts +24 -0
- package/dist/types/core/config.d.ts +35 -0
- package/dist/types/core/data.d.ts +129 -0
- package/dist/types/core/define-components.d.ts +27 -0
- package/dist/types/core/define-meta.d.ts +16 -0
- package/dist/types/core/deployment-env.d.ts +9 -0
- package/dist/types/core/diagnostics.d.ts +17 -0
- package/dist/types/core/i18n-ui.d.ts +500 -0
- package/dist/types/core/load-module.d.ts +7 -0
- package/dist/types/core/package-root.d.ts +17 -0
- package/dist/types/core/project.d.ts +9 -0
- package/dist/types/core/schema.d.ts +3451 -0
- package/dist/types/core/sources/types.d.ts +107 -0
- package/dist/types/core/types.d.ts +245 -0
- package/dist/types/core/ui-packs/ar.d.ts +3 -0
- package/dist/types/core/ui-packs/bg.d.ts +3 -0
- package/dist/types/core/ui-packs/bn.d.ts +3 -0
- package/dist/types/core/ui-packs/ca.d.ts +3 -0
- package/dist/types/core/ui-packs/cs.d.ts +3 -0
- package/dist/types/core/ui-packs/da.d.ts +3 -0
- package/dist/types/core/ui-packs/de.d.ts +3 -0
- package/dist/types/core/ui-packs/el.d.ts +3 -0
- package/dist/types/core/ui-packs/es.d.ts +3 -0
- package/dist/types/core/ui-packs/fa.d.ts +3 -0
- package/dist/types/core/ui-packs/fi.d.ts +3 -0
- package/dist/types/core/ui-packs/fr.d.ts +3 -0
- package/dist/types/core/ui-packs/he.d.ts +3 -0
- package/dist/types/core/ui-packs/hi.d.ts +3 -0
- package/dist/types/core/ui-packs/hr.d.ts +3 -0
- package/dist/types/core/ui-packs/hu.d.ts +3 -0
- package/dist/types/core/ui-packs/id.d.ts +3 -0
- package/dist/types/core/ui-packs/index.d.ts +13 -0
- package/dist/types/core/ui-packs/it.d.ts +3 -0
- package/dist/types/core/ui-packs/ja.d.ts +3 -0
- package/dist/types/core/ui-packs/ko.d.ts +3 -0
- package/dist/types/core/ui-packs/nl.d.ts +3 -0
- package/dist/types/core/ui-packs/no.d.ts +3 -0
- package/dist/types/core/ui-packs/pl.d.ts +3 -0
- package/dist/types/core/ui-packs/pt-br.d.ts +3 -0
- package/dist/types/core/ui-packs/pt.d.ts +3 -0
- package/dist/types/core/ui-packs/ro.d.ts +3 -0
- package/dist/types/core/ui-packs/ru.d.ts +3 -0
- package/dist/types/core/ui-packs/sk.d.ts +3 -0
- package/dist/types/core/ui-packs/sr.d.ts +3 -0
- package/dist/types/core/ui-packs/sv.d.ts +3 -0
- package/dist/types/core/ui-packs/th.d.ts +3 -0
- package/dist/types/core/ui-packs/tr.d.ts +3 -0
- package/dist/types/core/ui-packs/uk.d.ts +3 -0
- package/dist/types/core/ui-packs/vi.d.ts +3 -0
- package/dist/types/core/ui-packs/zh-tw.d.ts +3 -0
- package/dist/types/core/ui-packs/zh.d.ts +3 -0
- package/dist/types/core/version.d.ts +8 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/migrate/mintlify/config.d.ts +2 -0
- package/dist/types/migrate/mintlify/i18n.d.ts +7 -0
- package/dist/types/theme/fonts.d.ts +163 -0
- package/docs/01-quickstart.mdx +99 -0
- package/docs/02-deployment.mdx +129 -0
- package/docs/advanced/api-reference.mdx +114 -0
- package/docs/advanced/blog.mdx +121 -0
- package/docs/advanced/changelog.mdx +113 -0
- package/docs/advanced/custom-pages.mdx +268 -0
- package/docs/advanced/meta.ts +7 -0
- package/docs/changelog/v0-1-0.mdx +12 -0
- package/docs/changelog/v0-2-0.mdx +16 -0
- package/docs/configuration/ai.mdx +228 -0
- package/docs/configuration/analytics.mdx +98 -0
- package/docs/configuration/customization.mdx +91 -0
- package/docs/configuration/export.mdx +70 -0
- package/docs/configuration/index.mdx +290 -0
- package/docs/configuration/meta.ts +15 -0
- package/docs/configuration/search.mdx +172 -0
- package/docs/configuration/seo.mdx +196 -0
- package/docs/configuration/theming.mdx +178 -0
- package/docs/content/components.mdx +651 -0
- package/docs/content/i18n.mdx +205 -0
- package/docs/content/index.mdx +161 -0
- package/docs/content/islands.mdx +94 -0
- package/docs/content/meta.mdx +119 -0
- package/docs/content/meta.ts +15 -0
- package/docs/content/navigation.mdx +168 -0
- package/docs/content/sources.mdx +216 -0
- package/docs/content/syntax.mdx +445 -0
- package/docs/index.mdx +112 -0
- package/docs/reference/cli.mdx +43 -0
- package/docs/reference/frontmatter.mdx +74 -0
- package/docs/reference/meta.ts +7 -0
- package/package.json +150 -6
- package/src/ai/ask.ts +93 -0
- package/src/ai/llms.ts +64 -0
- package/src/ai/markdown.ts +31 -0
- package/src/ai/mcp/data.ts +74 -0
- package/src/ai/mcp/discovery.ts +49 -0
- package/src/ai/mcp/server.ts +225 -0
- package/src/ai/mcp/tools.ts +47 -0
- package/src/assets/icon.png +0 -0
- package/src/astro/examples.ts +104 -0
- package/src/astro/generate.ts +1006 -0
- package/src/astro/index.ts +4 -0
- package/src/astro/integration.ts +74 -0
- package/src/astro/islands.ts +131 -0
- package/src/astro/markdown-negotiation.ts +68 -0
- package/src/astro/pages.ts +87 -0
- package/src/astro/templates.ts +1326 -0
- package/src/cli/commands/add.ts +81 -0
- package/src/cli/commands/build.ts +103 -0
- package/src/cli/commands/dev.ts +108 -0
- package/src/cli/commands/doctor.ts +74 -0
- package/src/cli/commands/eject.ts +57 -0
- package/src/cli/commands/init.ts +98 -0
- package/src/cli/commands/migrate.ts +39 -0
- package/src/cli/commands/preview.ts +39 -0
- package/src/cli/commands/sync.ts +52 -0
- package/src/cli/commands/validate.ts +61 -0
- package/src/cli/index.ts +35 -0
- package/src/cli/log.ts +37 -0
- package/src/cli/prepare.ts +80 -0
- package/src/components/Icon.astro +99 -0
- package/src/components/content/Accordion.astro +8 -0
- package/src/components/content/AccordionItem.astro +121 -0
- package/src/components/content/AutoTypeTable.astro +51 -0
- package/src/components/content/Badge.astro +124 -0
- package/src/components/content/Callout.astro +73 -0
- package/src/components/content/Card.astro +104 -0
- package/src/components/content/CardGroup.astro +14 -0
- package/src/components/content/CodeBlock.astro +28 -0
- package/src/components/content/CodeGroup.astro +13 -0
- package/src/components/content/Color.astro +15 -0
- package/src/components/content/ColorItem.astro +87 -0
- package/src/components/content/ColorRow.astro +10 -0
- package/src/components/content/Column.astro +6 -0
- package/src/components/content/Columns.astro +9 -0
- package/src/components/content/Component.astro +65 -0
- package/src/components/content/Diff.astro +44 -0
- package/src/components/content/Expandable.astro +11 -0
- package/src/components/content/FileTree.astro +8 -0
- package/src/components/content/Frame.astro +70 -0
- package/src/components/content/GithubInfo.astro +110 -0
- package/src/components/content/Math.astro +24 -0
- package/src/components/content/Panel.astro +20 -0
- package/src/components/content/Prompt.astro +129 -0
- package/src/components/content/Step.astro +34 -0
- package/src/components/content/Steps.astro +20 -0
- package/src/components/content/Tab.astro +46 -0
- package/src/components/content/Tabs.astro +273 -0
- package/src/components/content/Tile.astro +42 -0
- package/src/components/content/Tooltip.astro +68 -0
- package/src/components/content/Tree.astro +300 -0
- package/src/components/content/TreeFile.astro +15 -0
- package/src/components/content/TreeFolder.astro +62 -0
- package/src/components/content/TypeTable.astro +106 -0
- package/src/components/content/Update.astro +66 -0
- package/src/components/content/Visibility.astro +12 -0
- package/src/components/content/Warning.astro +9 -0
- package/src/components/content/auto-type-table.ts +141 -0
- package/src/components/content/diff.ts +95 -0
- package/src/components/content/github-info.ts +79 -0
- package/src/components/content/mermaid-element.ts +68 -0
- package/src/components/github-mark.ts +9 -0
- package/src/components/index.ts +14 -0
- package/src/components/islands/AskAI.astro +12 -0
- package/src/components/islands/ask-ai.tsx +156 -0
- package/src/components/layout/Analytics.astro +63 -0
- package/src/components/layout/Banner.astro +50 -0
- package/src/components/layout/Breadcrumbs.astro +31 -0
- package/src/components/layout/Favicon.astro +22 -0
- package/src/components/layout/Fonts.astro +14 -0
- package/src/components/layout/Header.astro +188 -0
- package/src/components/layout/LanguageSwitcher.astro +56 -0
- package/src/components/layout/NavTree.astro +462 -0
- package/src/components/layout/PageActions.astro +438 -0
- package/src/components/layout/PageFeedback.astro +58 -0
- package/src/components/layout/PageLayout.astro +173 -0
- package/src/components/layout/Pagination.astro +56 -0
- package/src/components/layout/ReferenceLayout.astro +107 -0
- package/src/components/layout/RootLayout.astro +537 -0
- package/src/components/layout/Search.astro +608 -0
- package/src/components/layout/TableOfContents.astro +68 -0
- package/src/components/layout/analytics-client.ts +38 -0
- package/src/components/layout/head-scripts.ts +19 -0
- package/src/components/layout/nav-utils.ts +87 -0
- package/src/components/layout/overrides.ts +32 -0
- package/src/components/layout/search/algolia.ts +43 -0
- package/src/components/layout/search/endpoint.ts +22 -0
- package/src/components/layout/search/flexsearch.ts +52 -0
- package/src/components/layout/search/orama-cloud.ts +41 -0
- package/src/components/layout/search/orama.ts +26 -0
- package/src/components/layout/search/pagefind.ts +43 -0
- package/src/components/layout/search/types.ts +163 -0
- package/src/components/layout/search/typesense.ts +60 -0
- package/src/components/layout/toc-element.ts +108 -0
- package/src/core/bridge.ts +92 -0
- package/src/core/config.ts +112 -0
- package/src/core/content.ts +50 -0
- package/src/core/data.ts +128 -0
- package/src/core/define-components.ts +34 -0
- package/src/core/define-meta.ts +20 -0
- package/src/core/deployment-env.ts +73 -0
- package/src/core/diagnostics.ts +104 -0
- package/src/core/frontmatter.ts +43 -0
- package/src/core/graph.ts +128 -0
- package/src/core/i18n-ui.ts +171 -0
- package/src/core/i18n.ts +169 -0
- package/src/core/last-modified.ts +88 -0
- package/src/core/links.ts +352 -0
- package/src/core/load-module.ts +15 -0
- package/src/core/manifest.ts +126 -0
- package/src/core/meta.ts +97 -0
- package/src/core/navigation.ts +392 -0
- package/src/core/package-root.ts +37 -0
- package/src/core/project-graph.ts +153 -0
- package/src/core/project.ts +56 -0
- package/src/core/schema.ts +1057 -0
- package/src/core/server-features.ts +23 -0
- package/src/core/sources/assets.ts +77 -0
- package/src/core/sources/cache.ts +122 -0
- package/src/core/sources/filesystem.ts +99 -0
- package/src/core/sources/mdx-remote.ts +215 -0
- package/src/core/sources/mintlify.ts +161 -0
- package/src/core/sources/normalize.ts +243 -0
- package/src/core/sources/notion.ts +440 -0
- package/src/core/sources/portable-text.ts +143 -0
- package/src/core/sources/read.ts +36 -0
- package/src/core/sources/resolve.ts +158 -0
- package/src/core/sources/sanity.ts +218 -0
- package/src/core/sources/types.ts +105 -0
- package/src/core/tsconfig-aliases.ts +201 -0
- package/src/core/types.ts +261 -0
- package/src/core/ui-packs/ar.ts +47 -0
- package/src/core/ui-packs/bg.ts +47 -0
- package/src/core/ui-packs/bn.ts +47 -0
- package/src/core/ui-packs/ca.ts +47 -0
- package/src/core/ui-packs/cs.ts +47 -0
- package/src/core/ui-packs/da.ts +47 -0
- package/src/core/ui-packs/de.ts +47 -0
- package/src/core/ui-packs/el.ts +47 -0
- package/src/core/ui-packs/es.ts +47 -0
- package/src/core/ui-packs/fa.ts +47 -0
- package/src/core/ui-packs/fi.ts +47 -0
- package/src/core/ui-packs/fr.ts +47 -0
- package/src/core/ui-packs/he.ts +47 -0
- package/src/core/ui-packs/hi.ts +47 -0
- package/src/core/ui-packs/hr.ts +47 -0
- package/src/core/ui-packs/hu.ts +47 -0
- package/src/core/ui-packs/id.ts +47 -0
- package/src/core/ui-packs/index.ts +87 -0
- package/src/core/ui-packs/it.ts +47 -0
- package/src/core/ui-packs/ja.ts +47 -0
- package/src/core/ui-packs/ko.ts +47 -0
- package/src/core/ui-packs/nl.ts +47 -0
- package/src/core/ui-packs/no.ts +47 -0
- package/src/core/ui-packs/pl.ts +47 -0
- package/src/core/ui-packs/pt-br.ts +47 -0
- package/src/core/ui-packs/pt.ts +47 -0
- package/src/core/ui-packs/ro.ts +47 -0
- package/src/core/ui-packs/ru.ts +47 -0
- package/src/core/ui-packs/sk.ts +47 -0
- package/src/core/ui-packs/sr.ts +47 -0
- package/src/core/ui-packs/sv.ts +47 -0
- package/src/core/ui-packs/th.ts +47 -0
- package/src/core/ui-packs/tr.ts +47 -0
- package/src/core/ui-packs/uk.ts +47 -0
- package/src/core/ui-packs/vi.ts +47 -0
- package/src/core/ui-packs/zh-tw.ts +47 -0
- package/src/core/ui-packs/zh.ts +47 -0
- package/src/core/version.ts +23 -0
- package/src/deploy/robots.ts +20 -0
- package/src/deploy/rss.ts +128 -0
- package/src/deploy/sitemap.ts +28 -0
- package/src/index.ts +39 -0
- package/src/markdown/code-title.ts +71 -0
- package/src/markdown/directives.ts +83 -0
- package/src/markdown/heading-anchors.ts +137 -0
- package/src/markdown/index.ts +228 -0
- package/src/markdown/inline-code.ts +108 -0
- package/src/markdown/language-icon.ts +172 -0
- package/src/markdown/math.ts +32 -0
- package/src/markdown/mdast.ts +48 -0
- package/src/markdown/mermaid.ts +37 -0
- package/src/markdown/package-commands.ts +159 -0
- package/src/markdown/package-install.ts +40 -0
- package/src/migrate/fumadocs/config.ts +155 -0
- package/src/migrate/fumadocs/content.ts +365 -0
- package/src/migrate/fumadocs/frontmatter.ts +18 -0
- package/src/migrate/fumadocs/groups.ts +230 -0
- package/src/migrate/fumadocs/index.ts +337 -0
- package/src/migrate/fumadocs/meta.ts +244 -0
- package/src/migrate/migrate.ts +53 -0
- package/src/migrate/mintlify/config.ts +1040 -0
- package/src/migrate/mintlify/content.ts +98 -0
- package/src/migrate/mintlify/frontmatter.ts +126 -0
- package/src/migrate/mintlify/i18n.ts +51 -0
- package/src/migrate/mintlify/icons.ts +128 -0
- package/src/migrate/mintlify/index.ts +266 -0
- package/src/migrate/mintlify/snippets.ts +306 -0
- package/src/migrate/mintlify/transform.ts +80 -0
- package/src/migrate/nextra/content.ts +46 -0
- package/src/migrate/nextra/frontmatter.ts +40 -0
- package/src/migrate/nextra/index.ts +374 -0
- package/src/migrate/nextra/meta.ts +266 -0
- package/src/migrate/shared.ts +720 -0
- package/src/migrate/starlight/config.ts +459 -0
- package/src/migrate/starlight/content.ts +78 -0
- package/src/migrate/starlight/frontmatter.ts +111 -0
- package/src/migrate/starlight/i18n.ts +54 -0
- package/src/migrate/starlight/index.ts +131 -0
- package/src/og/card.ts +92 -0
- package/src/og/index.ts +2 -0
- package/src/openapi/scalar.ts +246 -0
- package/src/registry/eject.ts +310 -0
- package/src/registry/registry.ts +100 -0
- package/src/registry/rewrite-imports.ts +39 -0
- package/src/runtime/index.ts +14 -0
- package/src/search/build.ts +23 -0
- package/src/search/documents.ts +164 -0
- package/src/search/orama-index.ts +66 -0
- package/src/search/providers.ts +91 -0
- package/src/search/sync/algolia.ts +30 -0
- package/src/search/sync/index.ts +50 -0
- package/src/search/sync/orama-cloud.ts +40 -0
- package/src/search/sync/typesense.ts +65 -0
- package/src/seo/jsonld.ts +113 -0
- package/src/theme/entry.ts +637 -0
- package/src/theme/fonts.ts +198 -0
- package/src/theme/icons.ts +184 -0
- package/src/theme/palette.ts +143 -0
- package/src/theme/twoslash.ts +81 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { create, insertMultiple, search } from "@orama/orama";
|
|
2
|
+
import type { AnyOrama } from "@orama/orama";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The minimal document shape both the client-side search dialog and the
|
|
6
|
+
* server-side MCP `search_docs` tool index. Mirrors the `blume-search.json`
|
|
7
|
+
* entries built by `buildSearchDocuments`.
|
|
8
|
+
*/
|
|
9
|
+
export interface OramaDoc {
|
|
10
|
+
content: string;
|
|
11
|
+
description: string;
|
|
12
|
+
route: string;
|
|
13
|
+
title: string;
|
|
14
|
+
/** Locale code; indexed as an enum so queries can filter to one language. */
|
|
15
|
+
locale?: string;
|
|
16
|
+
/** Carried through for the search dialog's breadcrumb + filter pills. Stored
|
|
17
|
+
* but not indexed, so they ride along on the returned document untouched. */
|
|
18
|
+
breadcrumb?: string[];
|
|
19
|
+
section?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const SCHEMA = {
|
|
23
|
+
content: "string",
|
|
24
|
+
description: "string",
|
|
25
|
+
// Enum (not full-text "string") so `where` does an exact-match filter.
|
|
26
|
+
locale: "enum",
|
|
27
|
+
route: "string",
|
|
28
|
+
title: "string",
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
31
|
+
/** Title and description outrank body text, matching the search dialog. */
|
|
32
|
+
const BOOST = { description: 2, title: 3 };
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Build an in-memory Orama full-text index from search documents. Shared by the
|
|
36
|
+
* Orama client loader (browser) and the MCP server (Node), so ranking is
|
|
37
|
+
* identical wherever docs are queried.
|
|
38
|
+
*/
|
|
39
|
+
export const buildOramaIndex = async (
|
|
40
|
+
documents: OramaDoc[]
|
|
41
|
+
): Promise<AnyOrama> => {
|
|
42
|
+
const db = create({ schema: SCHEMA });
|
|
43
|
+
await insertMultiple(db, documents);
|
|
44
|
+
return db;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Query the index, returning the matching documents (highest-ranked first).
|
|
49
|
+
* When `locale` is given, results are filtered to that language via an exact
|
|
50
|
+
* `where` match on the `locale` enum.
|
|
51
|
+
*/
|
|
52
|
+
export const queryOramaIndex = async (
|
|
53
|
+
db: AnyOrama,
|
|
54
|
+
term: string,
|
|
55
|
+
limit: number,
|
|
56
|
+
locale?: string
|
|
57
|
+
): Promise<OramaDoc[]> => {
|
|
58
|
+
const found = await search(db, {
|
|
59
|
+
boost: BOOST,
|
|
60
|
+
limit,
|
|
61
|
+
properties: ["title", "description", "content"],
|
|
62
|
+
term,
|
|
63
|
+
...(locale ? { where: { locale: { eq: locale } } } : {}),
|
|
64
|
+
});
|
|
65
|
+
return found.hits.map((hit) => hit.document as unknown as OramaDoc);
|
|
66
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { SearchProvider } from "../core/schema.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* How a search provider integrates with the generated runtime.
|
|
5
|
+
*
|
|
6
|
+
* - `static` providers ship a client-side index (`blume-search.json`) that the
|
|
7
|
+
* browser loads and queries — `orama`, `flexsearch`.
|
|
8
|
+
* - `pagefind` builds its own index from the rendered HTML after the Astro build.
|
|
9
|
+
* - `hosted` providers are queried directly from the browser against an external
|
|
10
|
+
* service, with a build-time sync uploading the index — `algolia`,
|
|
11
|
+
* `orama-cloud`, `typesense`.
|
|
12
|
+
* - `server` providers proxy queries through a generated `/api/search` endpoint
|
|
13
|
+
* that holds a secret key, so they require server output — `mixedbread`.
|
|
14
|
+
*/
|
|
15
|
+
export type SearchProviderKind =
|
|
16
|
+
| "static"
|
|
17
|
+
| "pagefind"
|
|
18
|
+
| "hosted"
|
|
19
|
+
| "server"
|
|
20
|
+
| "none";
|
|
21
|
+
|
|
22
|
+
export interface SearchProviderMeta {
|
|
23
|
+
kind: SearchProviderKind;
|
|
24
|
+
/** Extra packages the generated `.blume/package.json` must declare. */
|
|
25
|
+
runtimeDeps: string[];
|
|
26
|
+
/** Whether the provider needs `deployment.output: "server"`. */
|
|
27
|
+
requiresServer: boolean;
|
|
28
|
+
/** Whether a build-time sync uploads the index to an external service. */
|
|
29
|
+
syncs: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const SEARCH_PROVIDERS: Record<SearchProvider, SearchProviderMeta> = {
|
|
33
|
+
algolia: {
|
|
34
|
+
kind: "hosted",
|
|
35
|
+
requiresServer: false,
|
|
36
|
+
runtimeDeps: ["algoliasearch"],
|
|
37
|
+
syncs: true,
|
|
38
|
+
},
|
|
39
|
+
flexsearch: {
|
|
40
|
+
kind: "static",
|
|
41
|
+
requiresServer: false,
|
|
42
|
+
runtimeDeps: ["flexsearch"],
|
|
43
|
+
syncs: false,
|
|
44
|
+
},
|
|
45
|
+
mixedbread: {
|
|
46
|
+
// Content is synced to the store out-of-band via the `mxbai` CLI, not by an
|
|
47
|
+
// in-process build step.
|
|
48
|
+
kind: "server",
|
|
49
|
+
requiresServer: true,
|
|
50
|
+
runtimeDeps: ["@mixedbread/sdk"],
|
|
51
|
+
syncs: false,
|
|
52
|
+
},
|
|
53
|
+
none: {
|
|
54
|
+
kind: "none",
|
|
55
|
+
requiresServer: false,
|
|
56
|
+
runtimeDeps: [],
|
|
57
|
+
syncs: false,
|
|
58
|
+
},
|
|
59
|
+
orama: {
|
|
60
|
+
kind: "static",
|
|
61
|
+
requiresServer: false,
|
|
62
|
+
runtimeDeps: ["@orama/orama"],
|
|
63
|
+
syncs: false,
|
|
64
|
+
},
|
|
65
|
+
"orama-cloud": {
|
|
66
|
+
kind: "hosted",
|
|
67
|
+
requiresServer: false,
|
|
68
|
+
runtimeDeps: ["@oramacloud/client"],
|
|
69
|
+
syncs: true,
|
|
70
|
+
},
|
|
71
|
+
pagefind: {
|
|
72
|
+
kind: "pagefind",
|
|
73
|
+
requiresServer: false,
|
|
74
|
+
runtimeDeps: [],
|
|
75
|
+
syncs: false,
|
|
76
|
+
},
|
|
77
|
+
typesense: {
|
|
78
|
+
kind: "hosted",
|
|
79
|
+
requiresServer: false,
|
|
80
|
+
runtimeDeps: ["typesense"],
|
|
81
|
+
syncs: true,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const searchProviderMeta = (
|
|
86
|
+
provider: SearchProvider
|
|
87
|
+
): SearchProviderMeta => SEARCH_PROVIDERS[provider];
|
|
88
|
+
|
|
89
|
+
/** Whether a provider ships a client-loaded `blume-search.json` index. */
|
|
90
|
+
export const servesStaticIndex = (provider: SearchProvider): boolean =>
|
|
91
|
+
SEARCH_PROVIDERS[provider].kind === "static";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { SearchRecord } from "../documents.ts";
|
|
2
|
+
|
|
3
|
+
export interface AlgoliaSyncConfig {
|
|
4
|
+
appId: string;
|
|
5
|
+
indexName: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Upload the search records to Algolia. Uses the admin key from
|
|
10
|
+
* `ALGOLIA_ADMIN_API_KEY` (never the config, which holds only the public,
|
|
11
|
+
* search-only key). Throws on a missing key/config so the caller can warn.
|
|
12
|
+
*/
|
|
13
|
+
export const syncAlgolia = async (
|
|
14
|
+
records: SearchRecord[],
|
|
15
|
+
config: AlgoliaSyncConfig | undefined
|
|
16
|
+
): Promise<void> => {
|
|
17
|
+
if (!config) {
|
|
18
|
+
throw new Error("search.algolia config is missing.");
|
|
19
|
+
}
|
|
20
|
+
const adminKey = process.env.ALGOLIA_ADMIN_API_KEY;
|
|
21
|
+
if (!adminKey) {
|
|
22
|
+
throw new Error("ALGOLIA_ADMIN_API_KEY is not set.");
|
|
23
|
+
}
|
|
24
|
+
const { algoliasearch } = await import("algoliasearch");
|
|
25
|
+
const client = algoliasearch(config.appId, adminKey);
|
|
26
|
+
await client.saveObjects({
|
|
27
|
+
indexName: config.indexName,
|
|
28
|
+
objects: records.map((record) => ({ ...record, objectID: record._id })),
|
|
29
|
+
});
|
|
30
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { BlumeProject } from "../../core/project-graph.ts";
|
|
2
|
+
import { buildSearchDocuments, toSearchRecords } from "../documents.ts";
|
|
3
|
+
import { searchProviderMeta } from "../providers.ts";
|
|
4
|
+
import { syncAlgolia } from "./algolia.ts";
|
|
5
|
+
import { syncOramaCloud } from "./orama-cloud.ts";
|
|
6
|
+
import { syncTypesense } from "./typesense.ts";
|
|
7
|
+
|
|
8
|
+
/** Minimal logger surface the build command provides. */
|
|
9
|
+
export interface SyncReporter {
|
|
10
|
+
start: (message: string) => void;
|
|
11
|
+
success: (message: string) => void;
|
|
12
|
+
warn: (message: string) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* After a build, upload the per-page records to the configured hosted provider.
|
|
17
|
+
* Reads secret admin keys from the environment; if a key (or config) is
|
|
18
|
+
* missing the sync is skipped with a warning rather than failing the build —
|
|
19
|
+
* CI may sync in a separate step. Providers without an in-process sync
|
|
20
|
+
* (Mixedbread, which uses the `mxbai` CLI) are no-ops here.
|
|
21
|
+
*/
|
|
22
|
+
export const syncSearchProvider = async (
|
|
23
|
+
project: BlumeProject,
|
|
24
|
+
reporter: SyncReporter
|
|
25
|
+
): Promise<void> => {
|
|
26
|
+
const { search } = project.config;
|
|
27
|
+
if (!searchProviderMeta(search.provider).syncs) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const records = toSearchRecords(await buildSearchDocuments(project));
|
|
32
|
+
reporter.start(`Syncing ${records.length} record(s) to ${search.provider}`);
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
// The `syncs` guard above admits only these three hosted providers, so an
|
|
36
|
+
// if-chain stays exhaustive without a dead `default` branch.
|
|
37
|
+
if (search.provider === "algolia") {
|
|
38
|
+
await syncAlgolia(records, search.algolia);
|
|
39
|
+
} else if (search.provider === "orama-cloud") {
|
|
40
|
+
await syncOramaCloud(records, search.oramaCloud);
|
|
41
|
+
} else if (search.provider === "typesense") {
|
|
42
|
+
await syncTypesense(records, search.typesense);
|
|
43
|
+
}
|
|
44
|
+
reporter.success(
|
|
45
|
+
`Synced ${records.length} record(s) to ${search.provider}`
|
|
46
|
+
);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
reporter.warn(`Search sync skipped: ${(error as Error).message}`);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { SearchRecord } from "../documents.ts";
|
|
2
|
+
|
|
3
|
+
export interface OramaCloudSyncConfig {
|
|
4
|
+
indexId?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Push the search records to an Orama Cloud index via `CloudManager`, using the
|
|
9
|
+
* private key from `ORAMA_PRIVATE_API_KEY`. Snapshots the full record set, then
|
|
10
|
+
* deploys. Throws on a missing key/index so the caller can warn.
|
|
11
|
+
*/
|
|
12
|
+
export const syncOramaCloud = async (
|
|
13
|
+
records: SearchRecord[],
|
|
14
|
+
config: OramaCloudSyncConfig | undefined
|
|
15
|
+
): Promise<void> => {
|
|
16
|
+
if (!config) {
|
|
17
|
+
throw new Error("search.oramaCloud config is missing.");
|
|
18
|
+
}
|
|
19
|
+
if (!config.indexId) {
|
|
20
|
+
throw new Error("search.oramaCloud.indexId is required to sync.");
|
|
21
|
+
}
|
|
22
|
+
const privateKey = process.env.ORAMA_PRIVATE_API_KEY;
|
|
23
|
+
if (!privateKey) {
|
|
24
|
+
throw new Error("ORAMA_PRIVATE_API_KEY is not set.");
|
|
25
|
+
}
|
|
26
|
+
const { CloudManager } = await import("@oramacloud/client");
|
|
27
|
+
const manager = new CloudManager({ api_key: privateKey });
|
|
28
|
+
const index = manager.index(config.indexId);
|
|
29
|
+
await index.snapshot(
|
|
30
|
+
records.map((record) => ({
|
|
31
|
+
content: record.content,
|
|
32
|
+
description: record.description,
|
|
33
|
+
id: record._id,
|
|
34
|
+
tag: record.tag,
|
|
35
|
+
title: record.title,
|
|
36
|
+
url: record.url,
|
|
37
|
+
}))
|
|
38
|
+
);
|
|
39
|
+
await index.deploy();
|
|
40
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { SearchRecord } from "../documents.ts";
|
|
2
|
+
|
|
3
|
+
export interface TypesenseSyncConfig {
|
|
4
|
+
collection: string;
|
|
5
|
+
host: string;
|
|
6
|
+
port?: number;
|
|
7
|
+
protocol?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Import the search records into a Typesense collection, creating the
|
|
12
|
+
* collection on first run. Uses the admin key from `TYPESENSE_ADMIN_API_KEY`.
|
|
13
|
+
* Throws on a missing key/config so the caller can warn.
|
|
14
|
+
*/
|
|
15
|
+
export const syncTypesense = async (
|
|
16
|
+
records: SearchRecord[],
|
|
17
|
+
config: TypesenseSyncConfig | undefined
|
|
18
|
+
): Promise<void> => {
|
|
19
|
+
if (!config) {
|
|
20
|
+
throw new Error("search.typesense config is missing.");
|
|
21
|
+
}
|
|
22
|
+
const adminKey = process.env.TYPESENSE_ADMIN_API_KEY;
|
|
23
|
+
if (!adminKey) {
|
|
24
|
+
throw new Error("TYPESENSE_ADMIN_API_KEY is not set.");
|
|
25
|
+
}
|
|
26
|
+
const { Client } = await import("typesense");
|
|
27
|
+
const client = new Client({
|
|
28
|
+
apiKey: adminKey,
|
|
29
|
+
nodes: [
|
|
30
|
+
{
|
|
31
|
+
host: config.host,
|
|
32
|
+
port: config.port ?? 443,
|
|
33
|
+
protocol: config.protocol ?? "https",
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
await client.collections(config.collection).retrieve();
|
|
40
|
+
} catch {
|
|
41
|
+
await client.collections().create({
|
|
42
|
+
fields: [
|
|
43
|
+
{ name: "title", type: "string" },
|
|
44
|
+
{ name: "description", optional: true, type: "string" },
|
|
45
|
+
{ name: "content", type: "string" },
|
|
46
|
+
{ name: "url", type: "string" },
|
|
47
|
+
{ facet: true, name: "tag", optional: true, type: "string" },
|
|
48
|
+
],
|
|
49
|
+
name: config.collection,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const documents = records.map((record) => ({
|
|
54
|
+
content: record.content,
|
|
55
|
+
description: record.description,
|
|
56
|
+
id: record._id,
|
|
57
|
+
tag: record.tag,
|
|
58
|
+
title: record.title,
|
|
59
|
+
url: record.url,
|
|
60
|
+
}));
|
|
61
|
+
await client
|
|
62
|
+
.collections(config.collection)
|
|
63
|
+
.documents()
|
|
64
|
+
.import(documents, { action: "upsert" });
|
|
65
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { Crumb } from "../components/layout/nav-utils.ts";
|
|
2
|
+
|
|
3
|
+
/** Inputs for a page's JSON-LD, all known at render time in RootLayout. */
|
|
4
|
+
export interface StructuredDataInput {
|
|
5
|
+
siteName: string;
|
|
6
|
+
/** Absolute site origin, or null when `deployment.site` is unset. */
|
|
7
|
+
siteUrl: string | null;
|
|
8
|
+
title: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
/** Page route, e.g. `/blog/post`. */
|
|
11
|
+
route: string;
|
|
12
|
+
/** Content type — `blog` and `changelog` map to richer article types. */
|
|
13
|
+
pageType?: string;
|
|
14
|
+
/** Publish date (string or YAML Date); emitted as ISO `datePublished`. */
|
|
15
|
+
published?: string | Date | null;
|
|
16
|
+
/** Last-modified date; emitted as ISO `dateModified`. */
|
|
17
|
+
modified?: string | Date | null;
|
|
18
|
+
/** BCP-47 language tag for `inLanguage`; defaults to `en`. */
|
|
19
|
+
locale?: string;
|
|
20
|
+
breadcrumbs: Crumb[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** schema.org `@type` for each content type; defaults to TechArticle. */
|
|
24
|
+
const ARTICLE_TYPES: Record<string, string> = {
|
|
25
|
+
blog: "BlogPosting",
|
|
26
|
+
changelog: "TechArticle",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const trimSlash = (value: string): string => value.replace(/\/$/u, "");
|
|
30
|
+
|
|
31
|
+
const absolute = (base: string | null, path: string): string =>
|
|
32
|
+
base ? `${base}${path}` : path;
|
|
33
|
+
|
|
34
|
+
const toIso = (value: string | Date | null | undefined): string | undefined => {
|
|
35
|
+
if (!value) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const date = new Date(value);
|
|
39
|
+
return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Build a schema.org JSON-LD `@graph` for a page: site identity, the page as an
|
|
44
|
+
* article, and its breadcrumb trail. Returns null when there is nothing useful
|
|
45
|
+
* to emit (e.g. the homepage without a configured site). URLs are absolute when
|
|
46
|
+
* `siteUrl` is set, otherwise route-relative.
|
|
47
|
+
*/
|
|
48
|
+
export const buildStructuredData = (
|
|
49
|
+
input: StructuredDataInput
|
|
50
|
+
): Record<string, unknown> | null => {
|
|
51
|
+
const base = input.siteUrl ? trimSlash(input.siteUrl) : null;
|
|
52
|
+
const pageUrl = absolute(base, input.route);
|
|
53
|
+
const graph: Record<string, unknown>[] = [];
|
|
54
|
+
|
|
55
|
+
if (base) {
|
|
56
|
+
graph.push({
|
|
57
|
+
"@id": `${base}#website`,
|
|
58
|
+
"@type": "WebSite",
|
|
59
|
+
name: input.siteName,
|
|
60
|
+
url: base,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// The homepage is fully described by the WebSite node; deeper pages get an
|
|
65
|
+
// article node plus a breadcrumb trail.
|
|
66
|
+
if (input.route !== "/") {
|
|
67
|
+
const node: Record<string, unknown> = {
|
|
68
|
+
"@id": `${pageUrl}#page`,
|
|
69
|
+
"@type": ARTICLE_TYPES[input.pageType ?? ""] ?? "TechArticle",
|
|
70
|
+
headline: input.title,
|
|
71
|
+
inLanguage: input.locale || "en",
|
|
72
|
+
name: input.title,
|
|
73
|
+
url: pageUrl,
|
|
74
|
+
};
|
|
75
|
+
if (input.description) {
|
|
76
|
+
node.description = input.description;
|
|
77
|
+
}
|
|
78
|
+
const published = toIso(input.published);
|
|
79
|
+
if (published) {
|
|
80
|
+
node.datePublished = published;
|
|
81
|
+
}
|
|
82
|
+
const modified = toIso(input.modified);
|
|
83
|
+
if (modified) {
|
|
84
|
+
node.dateModified = modified;
|
|
85
|
+
}
|
|
86
|
+
if (base) {
|
|
87
|
+
node.isPartOf = { "@id": `${base}#website` };
|
|
88
|
+
}
|
|
89
|
+
graph.push(node);
|
|
90
|
+
|
|
91
|
+
if (input.breadcrumbs.length > 1) {
|
|
92
|
+
graph.push({
|
|
93
|
+
"@type": "BreadcrumbList",
|
|
94
|
+
itemListElement: input.breadcrumbs.map((crumb, index) => {
|
|
95
|
+
const item: Record<string, unknown> = {
|
|
96
|
+
"@type": "ListItem",
|
|
97
|
+
name: crumb.label,
|
|
98
|
+
position: index + 1,
|
|
99
|
+
};
|
|
100
|
+
if (crumb.route) {
|
|
101
|
+
item.item = absolute(base, crumb.route);
|
|
102
|
+
}
|
|
103
|
+
return item;
|
|
104
|
+
}),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (graph.length === 0) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
return { "@context": "https://schema.org", "@graph": graph };
|
|
113
|
+
};
|