blume 0.0.0 → 0.1.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.
Files changed (263) hide show
  1. package/bin/blume.mjs +18 -0
  2. package/dist/cli/index.js +11989 -0
  3. package/dist/cli/index.js.map +141 -0
  4. package/docs/01-quickstart.mdx +99 -0
  5. package/docs/02-deployment.mdx +129 -0
  6. package/docs/advanced/api-reference.mdx +114 -0
  7. package/docs/advanced/blog.mdx +121 -0
  8. package/docs/advanced/changelog.mdx +113 -0
  9. package/docs/advanced/custom-pages.mdx +187 -0
  10. package/docs/advanced/meta.ts +7 -0
  11. package/docs/changelog/v0-1-0.mdx +12 -0
  12. package/docs/changelog/v0-2-0.mdx +16 -0
  13. package/docs/configuration/ai.mdx +228 -0
  14. package/docs/configuration/analytics.mdx +98 -0
  15. package/docs/configuration/customization.mdx +91 -0
  16. package/docs/configuration/export.mdx +70 -0
  17. package/docs/configuration/index.mdx +271 -0
  18. package/docs/configuration/meta.ts +15 -0
  19. package/docs/configuration/search.mdx +172 -0
  20. package/docs/configuration/seo.mdx +196 -0
  21. package/docs/configuration/theming.mdx +178 -0
  22. package/docs/content/components.mdx +565 -0
  23. package/docs/content/i18n.mdx +205 -0
  24. package/docs/content/index.mdx +161 -0
  25. package/docs/content/islands.mdx +94 -0
  26. package/docs/content/meta.mdx +119 -0
  27. package/docs/content/meta.ts +15 -0
  28. package/docs/content/navigation.mdx +168 -0
  29. package/docs/content/sources.mdx +216 -0
  30. package/docs/content/syntax.mdx +445 -0
  31. package/docs/index.mdx +112 -0
  32. package/docs/reference/cli.mdx +43 -0
  33. package/docs/reference/frontmatter.mdx +74 -0
  34. package/docs/reference/meta.ts +7 -0
  35. package/package.json +140 -6
  36. package/src/ai/ask.ts +93 -0
  37. package/src/ai/llms.ts +65 -0
  38. package/src/ai/markdown.ts +31 -0
  39. package/src/ai/mcp/data.ts +74 -0
  40. package/src/ai/mcp/discovery.ts +49 -0
  41. package/src/ai/mcp/server.ts +225 -0
  42. package/src/ai/mcp/tools.ts +47 -0
  43. package/src/assets/icon.png +0 -0
  44. package/src/astro/generate.ts +878 -0
  45. package/src/astro/index.ts +4 -0
  46. package/src/astro/integration.ts +74 -0
  47. package/src/astro/islands.ts +131 -0
  48. package/src/astro/markdown-negotiation.ts +68 -0
  49. package/src/astro/pages.ts +28 -0
  50. package/src/astro/templates.ts +1199 -0
  51. package/src/cli/commands/add.ts +81 -0
  52. package/src/cli/commands/build.ts +103 -0
  53. package/src/cli/commands/dev.ts +108 -0
  54. package/src/cli/commands/doctor.ts +74 -0
  55. package/src/cli/commands/eject.ts +57 -0
  56. package/src/cli/commands/init.ts +98 -0
  57. package/src/cli/commands/migrate.ts +39 -0
  58. package/src/cli/commands/preview.ts +39 -0
  59. package/src/cli/commands/sync.ts +52 -0
  60. package/src/cli/commands/validate.ts +60 -0
  61. package/src/cli/index.ts +35 -0
  62. package/src/cli/log.ts +37 -0
  63. package/src/cli/prepare.ts +80 -0
  64. package/src/components/Icon.astro +99 -0
  65. package/src/components/content/Accordion.astro +8 -0
  66. package/src/components/content/AccordionItem.astro +121 -0
  67. package/src/components/content/AutoTypeTable.astro +51 -0
  68. package/src/components/content/Badge.astro +124 -0
  69. package/src/components/content/Callout.astro +73 -0
  70. package/src/components/content/Card.astro +104 -0
  71. package/src/components/content/CardGroup.astro +14 -0
  72. package/src/components/content/CodeGroup.astro +13 -0
  73. package/src/components/content/Color.astro +15 -0
  74. package/src/components/content/ColorItem.astro +87 -0
  75. package/src/components/content/ColorRow.astro +10 -0
  76. package/src/components/content/Column.astro +6 -0
  77. package/src/components/content/Columns.astro +9 -0
  78. package/src/components/content/Expandable.astro +11 -0
  79. package/src/components/content/FileTree.astro +8 -0
  80. package/src/components/content/Frame.astro +70 -0
  81. package/src/components/content/GithubInfo.astro +110 -0
  82. package/src/components/content/Math.astro +24 -0
  83. package/src/components/content/Panel.astro +20 -0
  84. package/src/components/content/Prompt.astro +129 -0
  85. package/src/components/content/Step.astro +34 -0
  86. package/src/components/content/Steps.astro +20 -0
  87. package/src/components/content/Tab.astro +40 -0
  88. package/src/components/content/Tabs.astro +273 -0
  89. package/src/components/content/Tile.astro +42 -0
  90. package/src/components/content/Tooltip.astro +68 -0
  91. package/src/components/content/Tree.astro +300 -0
  92. package/src/components/content/TreeFile.astro +15 -0
  93. package/src/components/content/TreeFolder.astro +62 -0
  94. package/src/components/content/TypeTable.astro +106 -0
  95. package/src/components/content/Update.astro +66 -0
  96. package/src/components/content/Visibility.astro +12 -0
  97. package/src/components/content/Warning.astro +9 -0
  98. package/src/components/content/auto-type-table.ts +141 -0
  99. package/src/components/content/github-info.ts +79 -0
  100. package/src/components/content/mermaid-element.ts +68 -0
  101. package/src/components/github-mark.ts +9 -0
  102. package/src/components/index.ts +14 -0
  103. package/src/components/islands/AskAI.astro +12 -0
  104. package/src/components/islands/ask-ai.tsx +156 -0
  105. package/src/components/layout/Analytics.astro +63 -0
  106. package/src/components/layout/Banner.astro +50 -0
  107. package/src/components/layout/Breadcrumbs.astro +31 -0
  108. package/src/components/layout/Favicon.astro +15 -0
  109. package/src/components/layout/Fonts.astro +14 -0
  110. package/src/components/layout/Header.astro +188 -0
  111. package/src/components/layout/LanguageSwitcher.astro +56 -0
  112. package/src/components/layout/NavTree.astro +462 -0
  113. package/src/components/layout/PageActions.astro +438 -0
  114. package/src/components/layout/PageFeedback.astro +58 -0
  115. package/src/components/layout/Pagination.astro +56 -0
  116. package/src/components/layout/ReferenceLayout.astro +102 -0
  117. package/src/components/layout/RootLayout.astro +533 -0
  118. package/src/components/layout/Search.astro +608 -0
  119. package/src/components/layout/TableOfContents.astro +68 -0
  120. package/src/components/layout/analytics-client.ts +38 -0
  121. package/src/components/layout/nav-utils.ts +87 -0
  122. package/src/components/layout/overrides.ts +32 -0
  123. package/src/components/layout/search/algolia.ts +43 -0
  124. package/src/components/layout/search/endpoint.ts +22 -0
  125. package/src/components/layout/search/flexsearch.ts +52 -0
  126. package/src/components/layout/search/orama-cloud.ts +41 -0
  127. package/src/components/layout/search/orama.ts +26 -0
  128. package/src/components/layout/search/pagefind.ts +43 -0
  129. package/src/components/layout/search/types.ts +163 -0
  130. package/src/components/layout/search/typesense.ts +60 -0
  131. package/src/components/layout/toc-element.ts +108 -0
  132. package/src/core/bridge.ts +92 -0
  133. package/src/core/config.ts +112 -0
  134. package/src/core/content.ts +50 -0
  135. package/src/core/define-components.ts +34 -0
  136. package/src/core/define-meta.ts +20 -0
  137. package/src/core/deployment-env.ts +73 -0
  138. package/src/core/diagnostics.ts +104 -0
  139. package/src/core/graph.ts +128 -0
  140. package/src/core/i18n-ui.ts +171 -0
  141. package/src/core/i18n.ts +169 -0
  142. package/src/core/last-modified.ts +88 -0
  143. package/src/core/links.ts +336 -0
  144. package/src/core/load-module.ts +15 -0
  145. package/src/core/manifest.ts +126 -0
  146. package/src/core/meta.ts +97 -0
  147. package/src/core/navigation.ts +392 -0
  148. package/src/core/package-root.ts +37 -0
  149. package/src/core/project-graph.ts +153 -0
  150. package/src/core/project.ts +56 -0
  151. package/src/core/schema.ts +1057 -0
  152. package/src/core/server-features.ts +23 -0
  153. package/src/core/sources/assets.ts +77 -0
  154. package/src/core/sources/cache.ts +122 -0
  155. package/src/core/sources/filesystem.ts +99 -0
  156. package/src/core/sources/mdx-remote.ts +216 -0
  157. package/src/core/sources/mintlify.ts +161 -0
  158. package/src/core/sources/normalize.ts +227 -0
  159. package/src/core/sources/notion.ts +440 -0
  160. package/src/core/sources/portable-text.ts +143 -0
  161. package/src/core/sources/read.ts +36 -0
  162. package/src/core/sources/resolve.ts +158 -0
  163. package/src/core/sources/sanity.ts +218 -0
  164. package/src/core/sources/types.ts +105 -0
  165. package/src/core/types.ts +261 -0
  166. package/src/core/ui-packs/ar.ts +47 -0
  167. package/src/core/ui-packs/bg.ts +47 -0
  168. package/src/core/ui-packs/bn.ts +47 -0
  169. package/src/core/ui-packs/ca.ts +47 -0
  170. package/src/core/ui-packs/cs.ts +47 -0
  171. package/src/core/ui-packs/da.ts +47 -0
  172. package/src/core/ui-packs/de.ts +47 -0
  173. package/src/core/ui-packs/el.ts +47 -0
  174. package/src/core/ui-packs/es.ts +47 -0
  175. package/src/core/ui-packs/fa.ts +47 -0
  176. package/src/core/ui-packs/fi.ts +47 -0
  177. package/src/core/ui-packs/fr.ts +47 -0
  178. package/src/core/ui-packs/he.ts +47 -0
  179. package/src/core/ui-packs/hi.ts +47 -0
  180. package/src/core/ui-packs/hr.ts +47 -0
  181. package/src/core/ui-packs/hu.ts +47 -0
  182. package/src/core/ui-packs/id.ts +47 -0
  183. package/src/core/ui-packs/index.ts +87 -0
  184. package/src/core/ui-packs/it.ts +47 -0
  185. package/src/core/ui-packs/ja.ts +47 -0
  186. package/src/core/ui-packs/ko.ts +47 -0
  187. package/src/core/ui-packs/nl.ts +47 -0
  188. package/src/core/ui-packs/no.ts +47 -0
  189. package/src/core/ui-packs/pl.ts +47 -0
  190. package/src/core/ui-packs/pt-br.ts +47 -0
  191. package/src/core/ui-packs/pt.ts +47 -0
  192. package/src/core/ui-packs/ro.ts +47 -0
  193. package/src/core/ui-packs/ru.ts +47 -0
  194. package/src/core/ui-packs/sk.ts +47 -0
  195. package/src/core/ui-packs/sr.ts +47 -0
  196. package/src/core/ui-packs/sv.ts +47 -0
  197. package/src/core/ui-packs/th.ts +47 -0
  198. package/src/core/ui-packs/tr.ts +47 -0
  199. package/src/core/ui-packs/uk.ts +47 -0
  200. package/src/core/ui-packs/vi.ts +47 -0
  201. package/src/core/ui-packs/zh-tw.ts +47 -0
  202. package/src/core/ui-packs/zh.ts +47 -0
  203. package/src/core/version.ts +23 -0
  204. package/src/deploy/robots.ts +20 -0
  205. package/src/deploy/rss.ts +128 -0
  206. package/src/deploy/sitemap.ts +28 -0
  207. package/src/index.ts +27 -0
  208. package/src/markdown/code-title.ts +71 -0
  209. package/src/markdown/directives.ts +83 -0
  210. package/src/markdown/heading-anchors.ts +137 -0
  211. package/src/markdown/index.ts +159 -0
  212. package/src/markdown/inline-code.ts +108 -0
  213. package/src/markdown/language-icon.ts +172 -0
  214. package/src/markdown/math.ts +32 -0
  215. package/src/markdown/mdast.ts +48 -0
  216. package/src/markdown/mermaid.ts +37 -0
  217. package/src/markdown/package-commands.ts +159 -0
  218. package/src/markdown/package-install.ts +40 -0
  219. package/src/migrate/fumadocs/config.ts +106 -0
  220. package/src/migrate/fumadocs/content.ts +365 -0
  221. package/src/migrate/fumadocs/frontmatter.ts +18 -0
  222. package/src/migrate/fumadocs/index.ts +252 -0
  223. package/src/migrate/fumadocs/meta.ts +114 -0
  224. package/src/migrate/migrate.ts +53 -0
  225. package/src/migrate/mintlify/config.ts +1040 -0
  226. package/src/migrate/mintlify/content.ts +98 -0
  227. package/src/migrate/mintlify/frontmatter.ts +126 -0
  228. package/src/migrate/mintlify/i18n.ts +51 -0
  229. package/src/migrate/mintlify/icons.ts +128 -0
  230. package/src/migrate/mintlify/index.ts +266 -0
  231. package/src/migrate/mintlify/snippets.ts +305 -0
  232. package/src/migrate/mintlify/transform.ts +81 -0
  233. package/src/migrate/nextra/content.ts +46 -0
  234. package/src/migrate/nextra/frontmatter.ts +40 -0
  235. package/src/migrate/nextra/index.ts +374 -0
  236. package/src/migrate/nextra/meta.ts +266 -0
  237. package/src/migrate/shared.ts +623 -0
  238. package/src/migrate/starlight/config.ts +459 -0
  239. package/src/migrate/starlight/content.ts +78 -0
  240. package/src/migrate/starlight/frontmatter.ts +111 -0
  241. package/src/migrate/starlight/i18n.ts +54 -0
  242. package/src/migrate/starlight/index.ts +131 -0
  243. package/src/og/card.ts +92 -0
  244. package/src/og/index.ts +2 -0
  245. package/src/openapi/scalar.ts +246 -0
  246. package/src/registry/eject.ts +263 -0
  247. package/src/registry/registry.ts +100 -0
  248. package/src/registry/rewrite-imports.ts +39 -0
  249. package/src/runtime/index.ts +14 -0
  250. package/src/search/build.ts +23 -0
  251. package/src/search/documents.ts +165 -0
  252. package/src/search/orama-index.ts +66 -0
  253. package/src/search/providers.ts +91 -0
  254. package/src/search/sync/algolia.ts +30 -0
  255. package/src/search/sync/index.ts +50 -0
  256. package/src/search/sync/orama-cloud.ts +40 -0
  257. package/src/search/sync/typesense.ts +65 -0
  258. package/src/seo/jsonld.ts +113 -0
  259. package/src/theme/entry.ts +608 -0
  260. package/src/theme/fonts.ts +198 -0
  261. package/src/theme/icons.ts +184 -0
  262. package/src/theme/palette.ts +143 -0
  263. package/src/theme/twoslash.ts +81 -0
@@ -0,0 +1,533 @@
1
+ ---
2
+ import { EN_UI } from "../../core/i18n-ui.ts";
3
+ import type { UIStrings } from "../../core/i18n-ui.ts";
4
+ import type {
5
+ Heading,
6
+ LocaleSwitchOption,
7
+ Navigation,
8
+ } from "../../core/types.ts";
9
+ import "blume:theme";
10
+ import type { ComponentOverride } from "../../core/define-components.ts";
11
+ import { buildStructuredData } from "../../seo/jsonld.ts";
12
+ import Analytics from "./Analytics.astro";
13
+ import Banner from "./Banner.astro";
14
+ import Breadcrumbs from "./Breadcrumbs.astro";
15
+ import Favicon from "./Favicon.astro";
16
+ import Fonts from "./Fonts.astro";
17
+ import Header from "./Header.astro";
18
+ import { findBreadcrumbs, flattenPages, getPagination } from "./nav-utils.ts";
19
+ import NavTree from "./NavTree.astro";
20
+ import { resolveSlot } from "./overrides.ts";
21
+ import PageActions from "./PageActions.astro";
22
+ import PageFeedback from "./PageFeedback.astro";
23
+ import Pagination from "./Pagination.astro";
24
+ import TableOfContents from "./TableOfContents.astro";
25
+
26
+ interface Props {
27
+ site: { title: string; description?: string };
28
+ logo?: {
29
+ svg?: string;
30
+ light?: string;
31
+ dark?: string;
32
+ alt: string;
33
+ href: string;
34
+ } | null;
35
+ favicon?: {
36
+ href: string;
37
+ type?: string;
38
+ } | null;
39
+ banner?: {
40
+ content: string;
41
+ link?: { text: string; href: string };
42
+ dismissible: boolean;
43
+ key: string;
44
+ } | null;
45
+ analytics?: {
46
+ posthog?: { host?: string; key: string };
47
+ scripts?: {
48
+ attributes?: Record<string, string>;
49
+ content?: string;
50
+ src?: string;
51
+ strategy?: "async" | "defer";
52
+ }[];
53
+ vercel?: boolean;
54
+ } | null;
55
+ navigation: Navigation;
56
+ mcp?: { name: string; route: string } | null;
57
+ page: { title: string; description?: string; route: string };
58
+ headings: Heading[];
59
+ imageZoom?: boolean;
60
+ codeWrap?: boolean;
61
+ themeMode: "system" | "light" | "dark";
62
+ fontCssVars?: string[];
63
+ searchEnabled: boolean;
64
+ indexable: boolean;
65
+ ogImage?: string | null;
66
+ canonical?: string | null;
67
+ editUrl?: string | null;
68
+ askEnabled?: boolean;
69
+ /** Show the "Was this page helpful?" rating below the content. */
70
+ feedback?: boolean;
71
+ exportPdf?: boolean;
72
+ exportEpub?: boolean;
73
+ feeds?: { title: string; href: string }[];
74
+ siteUrl?: string | null;
75
+ pageType?: string;
76
+ published?: string | Date | null;
77
+ lastModified?: string | null;
78
+ noindex?: boolean;
79
+ structuredDataEnabled?: boolean;
80
+ /** Active locale code for `<html lang>` (defaults to `en`). */
81
+ locale?: string;
82
+ /** Text direction for `<html dir>` (defaults to `ltr`). */
83
+ dir?: "ltr" | "rtl";
84
+ /**
85
+ * Direction of the page content's own language — differs from `dir` on a
86
+ * fallback page (e.g. English content under an RTL locale). Applied to the
87
+ * article so prose punctuation reads correctly; the chrome still mirrors.
88
+ */
89
+ contentDir?: "ltr" | "rtl";
90
+ /** Resolved UI dictionary; English baseline when omitted. */
91
+ ui?: UIStrings;
92
+ /** `hreflang` alternates (absolute URLs) for this page's translations. */
93
+ localeAlternates?: { hreflang: string; href: string }[];
94
+ /** Absolute URL of the default-locale translation, for `hreflang=x-default`. */
95
+ xDefault?: string | null;
96
+ /** Language-switcher entries for the current page. */
97
+ localeSwitch?: LocaleSwitchOption[];
98
+ /**
99
+ * User layout-slot overrides from `components.ts` (`defineComponents`). Each
100
+ * key replaces the matching built-in; unknown keys are ignored. Wired slots:
101
+ * `Header`, `Sidebar`, `Breadcrumbs`, `TableOfContents`, `Pagination`.
102
+ */
103
+ layout?: Record<string, ComponentOverride>;
104
+ }
105
+
106
+ const {
107
+ site,
108
+ logo,
109
+ favicon,
110
+ banner,
111
+ analytics,
112
+ navigation,
113
+ mcp,
114
+ page,
115
+ headings,
116
+ imageZoom = true,
117
+ codeWrap = false,
118
+ themeMode,
119
+ fontCssVars,
120
+ searchEnabled,
121
+ indexable,
122
+ ogImage,
123
+ canonical,
124
+ editUrl,
125
+ askEnabled,
126
+ feedback = false,
127
+ exportPdf,
128
+ exportEpub,
129
+ feeds,
130
+ siteUrl,
131
+ pageType,
132
+ published,
133
+ lastModified,
134
+ noindex,
135
+ structuredDataEnabled,
136
+ locale = "en",
137
+ dir = "ltr",
138
+ contentDir,
139
+ ui,
140
+ localeAlternates,
141
+ xDefault,
142
+ localeSwitch,
143
+ layout = {},
144
+ } = Astro.props;
145
+
146
+ // Layout-slot overrides: render the user's component when configured, else the
147
+ // built-in. Each override receives the same props as the component it replaces.
148
+ const HeaderSlot = resolveSlot(layout.Header, Header);
149
+ const SidebarSlot = resolveSlot(layout.Sidebar, NavTree);
150
+ const BreadcrumbsSlot = resolveSlot(layout.Breadcrumbs, Breadcrumbs);
151
+ const TableOfContentsSlot = resolveSlot(layout.TableOfContents, TableOfContents);
152
+ const PaginationSlot = resolveSlot(layout.Pagination, Pagination);
153
+
154
+ const strings = ui ?? EN_UI;
155
+ // Filter search to the active language only when the site is multi-locale.
156
+ const searchLocale =
157
+ localeSwitch && localeSwitch.length > 1 ? locale : undefined;
158
+ const tocHeadings = headings.filter((h) => h.depth >= 2 && h.depth <= 3);
159
+ const pageTitle = page.title ? `${page.title} - ${site.title}` : site.title;
160
+ const description = page.description ?? site.description;
161
+
162
+ // "Last updated on <date>" — formatted in UTC to match the changelog timeline.
163
+ const lastModifiedDate = lastModified ? new Date(lastModified) : null;
164
+ const formattedLastModified =
165
+ lastModifiedDate && !Number.isNaN(lastModifiedDate.getTime())
166
+ ? new Intl.DateTimeFormat(locale || "en", {
167
+ dateStyle: "long",
168
+ timeZone: "UTC",
169
+ }).format(lastModifiedDate)
170
+ : null;
171
+
172
+ // The hosted MCP server's absolute URL, used by the page-actions install menu.
173
+ // Needs a configured site to be useful, so the menu is hidden without one.
174
+ const mcpUrl = mcp && siteUrl ? new URL(mcp.route, siteUrl).href : null;
175
+
176
+ const crumbs = findBreadcrumbs(navigation.sidebar, page.route);
177
+ const { prev, next } = getPagination(
178
+ flattenPages(navigation.sidebar),
179
+ page.route
180
+ );
181
+
182
+ // Structured data is skipped when disabled or for pages we ask crawlers not to
183
+ // index. Escape `<` so a title/description containing `</script>` can't break
184
+ // out of the inline JSON-LD tag.
185
+ const structuredData =
186
+ structuredDataEnabled === false || noindex
187
+ ? null
188
+ : buildStructuredData({
189
+ breadcrumbs: crumbs,
190
+ description,
191
+ locale,
192
+ modified: lastModified,
193
+ pageType,
194
+ published,
195
+ route: page.route,
196
+ siteName: site.title,
197
+ siteUrl: siteUrl ?? null,
198
+ title: page.title,
199
+ });
200
+ const structuredDataJson = structuredData
201
+ ? JSON.stringify(structuredData).replaceAll("<", "\\u003c")
202
+ : null;
203
+
204
+ const initialThemeScript = `(()=>{const m=${JSON.stringify(themeMode)};const s=localStorage.getItem("blume-theme");const sys=matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light";document.documentElement.dataset.theme=s??(m==="system"?sys:m);})();`;
205
+
206
+ // Pre-paint: hide a previously-dismissed banner before it flashes in.
207
+ const bannerScript = banner?.dismissible
208
+ ? `(()=>{if(localStorage.getItem("blume-banner:"+${JSON.stringify(banner.key)}))document.documentElement.setAttribute("data-blume-banner-hidden","");})();`
209
+ : null;
210
+ ---
211
+
212
+ <!doctype html>
213
+ <html dir={dir} lang={locale}>
214
+ <head>
215
+ <meta charset="utf-8" />
216
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
217
+ <title>{pageTitle}</title>
218
+ <Favicon favicon={favicon} />
219
+ <Fonts cssVars={fontCssVars ?? []} />
220
+ {canonical && <link rel="canonical" href={canonical} />}
221
+ {
222
+ localeAlternates?.map((alt) => (
223
+ <link href={alt.href} hreflang={alt.hreflang} rel="alternate" />
224
+ ))
225
+ }
226
+ {xDefault && <link href={xDefault} hreflang="x-default" rel="alternate" />}
227
+ {noindex && <meta name="robots" content="noindex" />}
228
+ {description && <meta name="description" content={description} />}
229
+ <meta property="og:title" content={pageTitle} />
230
+ {description && <meta property="og:description" content={description} />}
231
+ {
232
+ ogImage && (
233
+ <>
234
+ <meta property="og:image" content={ogImage} />
235
+ <meta name="twitter:image" content={ogImage} />
236
+ <meta name="twitter:card" content="summary_large_image" />
237
+ </>
238
+ )
239
+ }
240
+ {
241
+ feeds?.map((feed) => (
242
+ <link
243
+ href={feed.href}
244
+ rel="alternate"
245
+ title={feed.title}
246
+ type="application/rss+xml"
247
+ />
248
+ ))
249
+ }
250
+ {
251
+ structuredDataJson && (
252
+ <script
253
+ is:inline
254
+ set:html={structuredDataJson}
255
+ type="application/ld+json"
256
+ />
257
+ )
258
+ }
259
+ {bannerScript && <script is:inline set:html={bannerScript} />}
260
+ <script is:inline set:html={initialThemeScript} />
261
+ <Analytics analytics={analytics} />
262
+ </head>
263
+ <body
264
+ class="bg-background font-sans text-foreground antialiased"
265
+ data-blume-code-wrap={codeWrap ? "" : undefined}
266
+ data-blume-image-zoom={imageZoom ? "" : undefined}
267
+ >
268
+ <a
269
+ class="absolute start-[-999px] top-0 z-[100] bg-accent px-4 py-2 text-accent-foreground focus:start-0"
270
+ href="#blume-content">{strings.page.skipToContent}</a
271
+ >
272
+ <Banner banner={banner} />
273
+ <HeaderSlot
274
+ askEnabled={askEnabled}
275
+ localeSwitch={localeSwitch}
276
+ logo={logo}
277
+ navigation={navigation}
278
+ route={page.route}
279
+ searchEnabled={searchEnabled}
280
+ searchLocale={searchLocale}
281
+ searchStrings={strings.search}
282
+ site={site}
283
+ switcherStrings={strings.languageSwitcher}
284
+ >
285
+ <slot name="ask" slot="ask" />
286
+ </HeaderSlot>
287
+ <div class="mx-auto grid grid-cols-1 items-start lg:grid-cols-[17.5rem_minmax(0,1fr)] xl:grid-cols-[17.5rem_minmax(0,1fr)_17.5rem]">
288
+ <aside
289
+ aria-label="Primary"
290
+ class="fixed top-16 start-0 z-[35] h-[calc(100dvh-4rem)] w-64 max-w-[80vw] -translate-x-[105%] overflow-y-auto border-border border-e bg-background px-5 pt-4 pb-6 transition-transform rtl:translate-x-[105%] [:where([data-blume-nav-open])_&]:translate-x-0! lg:sticky lg:z-auto lg:w-auto lg:max-w-none lg:translate-x-0! lg:border-e-0 lg:bg-transparent lg:px-4"
291
+ >
292
+ <nav>
293
+ <SidebarSlot currentRoute={page.route} items={navigation.sidebar} />
294
+ </nav>
295
+ </aside>
296
+ <main class="min-w-0 px-6 pt-6 pb-10 lg:px-8 xl:px-10" id="blume-content">
297
+ <BreadcrumbsSlot crumbs={crumbs} />
298
+ <TableOfContentsSlot
299
+ headings={tocHeadings}
300
+ title={strings.toc.title}
301
+ variant="mobile"
302
+ />
303
+ <article class="prose mx-auto max-w-[42rem]" dir={contentDir}>
304
+ <slot />
305
+ </article>
306
+ {
307
+ formattedLastModified && (
308
+ <p class="mx-auto mt-10 max-w-[42rem] text-muted-foreground text-sm">
309
+ {strings.page.lastUpdated} {formattedLastModified}
310
+ </p>
311
+ )
312
+ }
313
+ {feedback && <PageFeedback strings={strings.feedback} />}
314
+ <PaginationSlot next={next} prev={prev} strings={strings.page} />
315
+ </main>
316
+ <aside
317
+ aria-label={strings.toc.title}
318
+ class="sticky top-16 hidden h-[calc(100dvh-4rem)] overflow-y-auto px-4 pt-6 pb-10 text-sm xl:block"
319
+ >
320
+ <TableOfContentsSlot
321
+ headings={tocHeadings}
322
+ title={strings.toc.title}
323
+ variant="desktop"
324
+ />
325
+ <PageActions
326
+ askEnabled={askEnabled}
327
+ editUrl={editUrl}
328
+ exportEpub={exportEpub}
329
+ exportPdf={exportPdf}
330
+ mcpName={mcp?.name}
331
+ mcpUrl={mcpUrl}
332
+ route={page.route}
333
+ strings={strings.actions}
334
+ />
335
+ </aside>
336
+ </div>
337
+ <button
338
+ aria-label="Close navigation"
339
+ class="fixed inset-0 z-[30] hidden cursor-pointer border-0 bg-black/40 [:where([data-blume-nav-open])_&]:block lg:hidden"
340
+ data-blume-nav-toggle
341
+ type="button"
342
+ ></button>
343
+ <script>
344
+ import { icons } from "../../theme/icons.ts";
345
+ // Registers the <blume-mermaid> custom element (emitted by ```mermaid
346
+ // fences). Mermaid itself is lazy-loaded only on pages that use a diagram.
347
+ import "../content/mermaid-element.ts";
348
+ // Registers the <blume-toc> custom element: scrollspy for the table of
349
+ // contents, highlighting the section currently in view as you scroll.
350
+ import "./toc-element.ts";
351
+
352
+ const svg = (name: string) =>
353
+ `<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${icons[name]}</svg>`;
354
+
355
+ const buttonClass =
356
+ "absolute right-3 inline-flex size-7 items-center justify-center rounded-full bg-transparent text-muted-foreground opacity-70 transition hover:bg-muted hover:text-foreground hover:opacity-100 focus-visible:opacity-100";
357
+
358
+ const languageLabels: Record<string, string> = {
359
+ astro: "Astro",
360
+ bash: "Bash",
361
+ css: "CSS",
362
+ html: "HTML",
363
+ js: "JavaScript",
364
+ json: "JSON",
365
+ jsx: "JSX",
366
+ md: "Markdown",
367
+ mdx: "MDX",
368
+ plaintext: "Text",
369
+ sh: "Shell",
370
+ shell: "Shell",
371
+ ts: "TypeScript",
372
+ tsx: "TSX",
373
+ txt: "Text",
374
+ yaml: "YAML",
375
+ yml: "YAML",
376
+ zsh: "Zsh",
377
+ };
378
+
379
+ for (const pre of document.querySelectorAll(".prose pre")) {
380
+ if (pre.querySelector("[data-blume-copy]")) {
381
+ continue;
382
+ }
383
+ // Skip nested <pre>: Twoslash renders each hover popup's type signature
384
+ // as a <pre> inside the code block, which shouldn't get its own button.
385
+ if (pre.parentElement?.closest("pre")) {
386
+ continue;
387
+ }
388
+ const language = pre.getAttribute("data-language");
389
+ if (language) {
390
+ pre.setAttribute(
391
+ "data-language",
392
+ languageLabels[language.toLowerCase()] ?? language
393
+ );
394
+ }
395
+ pre.classList.add("group", "relative");
396
+ const button = document.createElement("button");
397
+ button.type = "button";
398
+ // The language-label bar (prose) vs flush code (tabs) need a different
399
+ // offset; pick the Tailwind class by context instead of a CSS override.
400
+ const topClass = pre.closest("blume-tabs, .not-prose")
401
+ ? "top-2.5"
402
+ : "top-2";
403
+ button.className = `${buttonClass} ${topClass}`;
404
+ button.setAttribute("data-blume-copy", "");
405
+ button.setAttribute("aria-label", "Copy code");
406
+ button.innerHTML = svg("copy");
407
+ button.addEventListener("click", async () => {
408
+ const text = pre.querySelector("code")?.textContent ?? "";
409
+ try {
410
+ await navigator.clipboard.writeText(text);
411
+ } catch {
412
+ return;
413
+ }
414
+ button.innerHTML = svg("check");
415
+ setTimeout(() => {
416
+ button.innerHTML = svg("copy");
417
+ }, 1500);
418
+ });
419
+ pre.appendChild(button);
420
+ }
421
+
422
+ // Click-to-zoom for content images (gated by `markdown.imageZoom`).
423
+ // Enhances every `.prose img` so plain Markdown images open in a lightbox.
424
+ // Opt out per-image with `data-no-zoom`.
425
+ const zoomImages: Iterable<HTMLImageElement> =
426
+ document.body.hasAttribute("data-blume-image-zoom")
427
+ ? document.querySelectorAll<HTMLImageElement>(
428
+ ".prose img:not([data-no-zoom])"
429
+ )
430
+ : [];
431
+ const zoomTargets = Array.from(zoomImages);
432
+ if (zoomTargets.length > 0) {
433
+ const reduceMotion = window.matchMedia(
434
+ "(prefers-reduced-motion: reduce)"
435
+ ).matches;
436
+ let active: {
437
+ clone: HTMLImageElement;
438
+ original: HTMLImageElement;
439
+ overlay: HTMLElement;
440
+ } | null = null;
441
+
442
+ const closeZoom = () => {
443
+ if (!active) {
444
+ return;
445
+ }
446
+ const { clone, original, overlay } = active;
447
+ active = null;
448
+ overlay.style.opacity = "0";
449
+ clone.style.transform = "translate(0px, 0px) scale(1)";
450
+ const cleanup = () => {
451
+ overlay.remove();
452
+ original.style.visibility = "";
453
+ document.removeEventListener("keydown", onKey);
454
+ window.removeEventListener("scroll", closeZoom);
455
+ };
456
+ if (reduceMotion) {
457
+ cleanup();
458
+ } else {
459
+ clone.addEventListener("transitionend", cleanup, { once: true });
460
+ }
461
+ };
462
+
463
+ const onKey = (event: KeyboardEvent) => {
464
+ if (event.key === "Escape") {
465
+ closeZoom();
466
+ }
467
+ };
468
+
469
+ const openZoom = (image: HTMLImageElement) => {
470
+ if (active) {
471
+ return;
472
+ }
473
+ const start = image.getBoundingClientRect();
474
+ if (start.width === 0 || start.height === 0) {
475
+ return;
476
+ }
477
+
478
+ const overlay = document.createElement("div");
479
+ overlay.className =
480
+ "fixed inset-0 z-[100] flex cursor-zoom-out items-center justify-center bg-background/80 opacity-0 backdrop-blur-sm transition-opacity duration-300";
481
+
482
+ const clone = image.cloneNode(true) as HTMLImageElement;
483
+ clone.className = "absolute m-0 max-w-none rounded-blume shadow-2xl";
484
+ clone.style.top = `${start.top}px`;
485
+ clone.style.left = `${start.left}px`;
486
+ clone.style.width = `${start.width}px`;
487
+ clone.style.height = `${start.height}px`;
488
+ clone.style.transformOrigin = "top left";
489
+ if (!reduceMotion) {
490
+ clone.style.transition =
491
+ "transform 300ms cubic-bezier(0.22, 1, 0.36, 1)";
492
+ }
493
+
494
+ overlay.appendChild(clone);
495
+ document.body.appendChild(overlay);
496
+ image.style.visibility = "hidden";
497
+ active = { clone, original: image, overlay };
498
+
499
+ const margin = 0.92;
500
+ const ratio = start.width / start.height;
501
+ let targetWidth = window.innerWidth * margin;
502
+ let targetHeight = targetWidth / ratio;
503
+ if (targetHeight > window.innerHeight * margin) {
504
+ targetHeight = window.innerHeight * margin;
505
+ targetWidth = targetHeight * ratio;
506
+ }
507
+ const naturalWidth = image.naturalWidth || targetWidth;
508
+ if (targetWidth > naturalWidth) {
509
+ targetWidth = naturalWidth;
510
+ targetHeight = targetWidth / ratio;
511
+ }
512
+ const scale = targetWidth / start.width;
513
+ const dx = (window.innerWidth - targetWidth) / 2 - start.left;
514
+ const dy = (window.innerHeight - targetHeight) / 2 - start.top;
515
+
516
+ requestAnimationFrame(() => {
517
+ overlay.style.opacity = "1";
518
+ clone.style.transform = `translate(${dx}px, ${dy}px) scale(${scale})`;
519
+ });
520
+
521
+ overlay.addEventListener("click", closeZoom);
522
+ document.addEventListener("keydown", onKey);
523
+ window.addEventListener("scroll", closeZoom, { passive: true });
524
+ };
525
+
526
+ for (const image of zoomTargets) {
527
+ image.classList.add("cursor-zoom-in");
528
+ image.addEventListener("click", () => openZoom(image));
529
+ }
530
+ }
531
+ </script>
532
+ </body>
533
+ </html>