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.
Files changed (194) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/dist/cli/index.js +3639 -1377
  3. package/dist/cli/index.js.map +103 -91
  4. package/dist/types/ai/component-markdown.d.ts +79 -0
  5. package/dist/types/components/layout/nav-utils.d.ts +60 -0
  6. package/dist/types/core/base-path.d.ts +9 -0
  7. package/dist/types/core/config-input.d.ts +206 -4
  8. package/dist/types/core/config.d.ts +6 -4
  9. package/dist/types/core/data.d.ts +23 -1
  10. package/dist/types/core/github.d.ts +35 -0
  11. package/dist/types/core/i18n-ui.d.ts +8 -0
  12. package/dist/types/core/navigation.d.ts +69 -0
  13. package/dist/types/core/schema.d.ts +117 -1
  14. package/dist/types/core/sources/types.d.ts +31 -6
  15. package/dist/types/core/types.d.ts +23 -2
  16. package/dist/types/markdown/features.d.ts +21 -0
  17. package/dist/types/openapi/references.d.ts +21 -1
  18. package/dist/types/seo/jsonld.d.ts +105 -0
  19. package/dist/types/theme/fonts.d.ts +34 -4
  20. package/docs/_snippets/include-demo.mdx +7 -0
  21. package/docs/advanced/api-reference.mdx +3 -3
  22. package/docs/advanced/custom-pages.mdx +1 -1
  23. package/docs/advanced/graphql.mdx +84 -0
  24. package/docs/advanced/meta.ts +8 -1
  25. package/docs/configuration/ai.mdx +21 -3
  26. package/docs/configuration/index.mdx +24 -0
  27. package/docs/configuration/search.mdx +13 -1
  28. package/docs/configuration/seo.mdx +27 -0
  29. package/docs/configuration/theming.mdx +17 -0
  30. package/docs/content/components.mdx +7 -0
  31. package/docs/content/includes.mdx +68 -0
  32. package/docs/content/meta.ts +1 -0
  33. package/docs/content/navigation.mdx +25 -0
  34. package/docs/content/sources.mdx +42 -1
  35. package/docs/content/syntax.mdx +69 -1
  36. package/docs/content/versioning.mdx +15 -9
  37. package/docs/reference/cli.mdx +2 -1
  38. package/package.json +23 -14
  39. package/skills/blume-migrate/SKILL.md +16 -7
  40. package/skills/blume-migrate/references/docusaurus.md +5 -3
  41. package/skills/blume-migrate/references/fumadocs.md +10 -2
  42. package/skills/blume-migrate/references/mintlify.md +3 -2
  43. package/skills/blume-migrate/references/nextra.md +2 -2
  44. package/skills/blume-migrate/references/starlight.md +1 -1
  45. package/src/ai/agent-readability.ts +2 -1
  46. package/src/ai/ask-data.ts +2 -1
  47. package/src/ai/component-markdown.ts +199 -36
  48. package/src/ai/llms.ts +93 -6
  49. package/src/ai/markdown.ts +2 -2
  50. package/src/ai/mcp/discovery.ts +10 -2
  51. package/src/ai/mcp/server.ts +74 -2
  52. package/src/astro/generate.ts +183 -116
  53. package/src/astro/include-hmr.ts +81 -0
  54. package/src/astro/include-refresh.ts +0 -0
  55. package/src/astro/index.ts +3 -5
  56. package/src/astro/templates.ts +125 -76
  57. package/src/cli/commands/build.ts +84 -15
  58. package/src/cli/init/questions.ts +1 -0
  59. package/src/cli/init/scaffold.ts +27 -4
  60. package/src/components/colors.ts +142 -0
  61. package/src/components/content/Badge.astro +5 -12
  62. package/src/components/content/Callout.astro +19 -36
  63. package/src/components/content/Card.astro +15 -21
  64. package/src/components/content/Component.astro +10 -1
  65. package/src/components/content/GithubInfo.astro +28 -9
  66. package/src/components/content/Tabs.astro +27 -5
  67. package/src/components/content/github-info.ts +20 -5
  68. package/src/components/dropdown-dismiss.ts +122 -0
  69. package/src/components/layout/Fonts.astro +15 -8
  70. package/src/components/layout/Header.astro +44 -0
  71. package/src/components/layout/LanguageSwitcher.astro +9 -1
  72. package/src/components/layout/NavSelector.astro +12 -3
  73. package/src/components/layout/NavTree.astro +6 -18
  74. package/src/components/layout/PageActions.astro +29 -8
  75. package/src/components/layout/PageLayout.astro +10 -1
  76. package/src/components/layout/ReferenceLayout.astro +6 -1
  77. package/src/components/layout/RootLayout.astro +46 -15
  78. package/src/components/layout/Search.astro +36 -4
  79. package/src/components/layout/TableOfContents.astro +8 -2
  80. package/src/components/layout/head-scripts.ts +53 -1
  81. package/src/components/openapi/ApiOverview.astro +13 -3
  82. package/src/components/openapi/AsyncApiOperation.astro +7 -14
  83. package/src/components/openapi/GraphqlChip.astro +33 -0
  84. package/src/components/openapi/GraphqlFieldsTable.astro +111 -0
  85. package/src/components/openapi/GraphqlOperation.astro +186 -0
  86. package/src/components/openapi/GraphqlType.astro +154 -0
  87. package/src/components/openapi/MethodBadge.astro +3 -14
  88. package/src/components/openapi/Operation.astro +12 -5
  89. package/src/components/openapi/OperationPanel.astro +43 -0
  90. package/src/components/openapi/RequestPanel.astro +5 -10
  91. package/src/components/openapi/Responses.astro +1 -16
  92. package/src/components/openapi/graphql-helpers.ts +466 -0
  93. package/src/components/openapi/playground-client.ts +15 -0
  94. package/src/components/openapi/sample-panels.ts +45 -0
  95. package/src/components/openapi/snippets.ts +13 -35
  96. package/src/core/base-path.ts +11 -0
  97. package/src/core/config-input.ts +209 -2
  98. package/src/core/config.ts +6 -4
  99. package/src/core/content-assets.ts +15 -4
  100. package/src/core/data.ts +18 -2
  101. package/src/core/diagnostics.ts +8 -0
  102. package/src/core/frontmatter.ts +20 -8
  103. package/src/core/github.ts +71 -0
  104. package/src/core/graph.ts +22 -8
  105. package/src/core/heading-markers.ts +96 -0
  106. package/src/core/i18n-ui.ts +11 -0
  107. package/src/core/includes.ts +632 -0
  108. package/src/core/last-modified.ts +36 -11
  109. package/src/core/links.ts +79 -13
  110. package/src/core/meta.ts +2 -1
  111. package/src/core/nav-diagnostics.ts +11 -2
  112. package/src/core/navigation.ts +27 -6
  113. package/src/core/project-graph.ts +61 -9
  114. package/src/core/schema.ts +226 -35
  115. package/src/core/server-features.ts +5 -9
  116. package/src/core/sources/github-releases.ts +2 -2
  117. package/src/core/sources/normalize.ts +502 -115
  118. package/src/core/sources/notion.ts +43 -8
  119. package/src/core/sources/obsidian.ts +1038 -0
  120. package/src/core/sources/read.ts +36 -1
  121. package/src/core/sources/resolve.ts +34 -1
  122. package/src/core/sources/types.ts +28 -6
  123. package/src/core/sources/watch.ts +12 -8
  124. package/src/core/tsconfig-aliases.ts +48 -35
  125. package/src/core/types.ts +25 -2
  126. package/src/core/ui-packs/ar.ts +1 -0
  127. package/src/core/ui-packs/bg.ts +2 -0
  128. package/src/core/ui-packs/bn.ts +1 -0
  129. package/src/core/ui-packs/ca.ts +2 -0
  130. package/src/core/ui-packs/cs.ts +1 -0
  131. package/src/core/ui-packs/da.ts +1 -0
  132. package/src/core/ui-packs/de.ts +2 -0
  133. package/src/core/ui-packs/el.ts +2 -0
  134. package/src/core/ui-packs/es.ts +2 -0
  135. package/src/core/ui-packs/fa.ts +1 -0
  136. package/src/core/ui-packs/fi.ts +1 -0
  137. package/src/core/ui-packs/fr.ts +2 -0
  138. package/src/core/ui-packs/he.ts +1 -0
  139. package/src/core/ui-packs/hi.ts +1 -0
  140. package/src/core/ui-packs/hr.ts +2 -0
  141. package/src/core/ui-packs/hu.ts +2 -0
  142. package/src/core/ui-packs/id.ts +2 -0
  143. package/src/core/ui-packs/it.ts +1 -0
  144. package/src/core/ui-packs/ja.ts +2 -0
  145. package/src/core/ui-packs/ko.ts +2 -0
  146. package/src/core/ui-packs/nl.ts +2 -0
  147. package/src/core/ui-packs/no.ts +2 -0
  148. package/src/core/ui-packs/pl.ts +2 -0
  149. package/src/core/ui-packs/pt-br.ts +2 -0
  150. package/src/core/ui-packs/pt.ts +2 -0
  151. package/src/core/ui-packs/ro.ts +2 -0
  152. package/src/core/ui-packs/ru.ts +2 -0
  153. package/src/core/ui-packs/sk.ts +1 -0
  154. package/src/core/ui-packs/sr.ts +1 -0
  155. package/src/core/ui-packs/sv.ts +2 -0
  156. package/src/core/ui-packs/th.ts +1 -0
  157. package/src/core/ui-packs/tr.ts +2 -0
  158. package/src/core/ui-packs/uk.ts +2 -0
  159. package/src/core/ui-packs/vi.ts +1 -0
  160. package/src/core/ui-packs/zh-tw.ts +1 -0
  161. package/src/core/ui-packs/zh.ts +1 -0
  162. package/src/core/version-cut.ts +21 -3
  163. package/src/core/yaml.ts +26 -0
  164. package/src/deploy/function-bundle.ts +251 -0
  165. package/src/eval/schema.ts +3 -1
  166. package/src/markdown/code-title.ts +22 -16
  167. package/src/markdown/features.ts +21 -0
  168. package/src/markdown/fence-meta.ts +50 -0
  169. package/src/markdown/heading-anchors.ts +198 -37
  170. package/src/markdown/include.ts +247 -0
  171. package/src/markdown/index.ts +43 -34
  172. package/src/markdown/language-icon.ts +2 -2
  173. package/src/markdown/mdast.ts +7 -3
  174. package/src/markdown/ts2js.ts +264 -0
  175. package/src/openapi/asyncapi.ts +4 -1
  176. package/src/openapi/graphql-build.ts +293 -0
  177. package/src/openapi/graphql.ts +212 -0
  178. package/src/openapi/model.ts +38 -5
  179. package/src/openapi/parse.ts +34 -0
  180. package/src/openapi/proxy.ts +30 -5
  181. package/src/openapi/references.ts +89 -13
  182. package/src/openapi/render-mdx.ts +48 -8
  183. package/src/openapi/scalar.ts +5 -12
  184. package/src/openapi/source.ts +91 -23
  185. package/src/registry/eject.ts +11 -0
  186. package/src/search/documents.ts +229 -37
  187. package/src/search/orama-index.ts +9 -5
  188. package/src/seo/jsonld.ts +293 -51
  189. package/src/theme/code-block-padding.ts +16 -0
  190. package/src/theme/entry.ts +65 -11
  191. package/src/theme/fonts.ts +189 -16
  192. package/src/translate/prompts.ts +2 -0
  193. package/src/translate/run.ts +7 -0
  194. package/src/translate/work-list.ts +0 -0
package/src/seo/jsonld.ts CHANGED
@@ -4,6 +4,62 @@ import { normalizeBasePath, withBasePath } from "../core/base-path.ts";
4
4
  /** A date-ish value carried through frontmatter (string, YAML Date, or unset). */
5
5
  type DateInput = string | Date | null;
6
6
 
7
+ /** A schema.org `PostalAddress`; every part optional. */
8
+ export interface PostalAddressIdentity {
9
+ addressCountry?: string;
10
+ addressLocality?: string;
11
+ addressRegion?: string;
12
+ postalCode?: string;
13
+ streetAddress?: string;
14
+ }
15
+
16
+ /**
17
+ * `seo.organization`: the organization behind the site, emitted on every page
18
+ * as an `Organization` node the WebSite and article nodes cite as publisher.
19
+ */
20
+ export interface OrganizationIdentity {
21
+ address?: PostalAddressIdentity;
22
+ /** `ContactPoint.contactType`; only emitted with an email or telephone. */
23
+ contactType: string;
24
+ email?: string;
25
+ /** Absolute URL or root-relative path (absolutized like page URLs). */
26
+ logo?: string;
27
+ /** Defaults to the site title. */
28
+ name?: string;
29
+ /** Profile URLs (GitHub, X, LinkedIn, …) that identify the organization. */
30
+ sameAs: string[];
31
+ telephone?: string;
32
+ /** Defaults to the site origin. */
33
+ url?: string;
34
+ }
35
+
36
+ /**
37
+ * `seo.software`: the product the site documents, emitted on the homepage as
38
+ * a `SoftwareApplication` node — the identity type agents use to tell what a
39
+ * docs site is about.
40
+ */
41
+ export interface SoftwareIdentity {
42
+ applicationCategory: string;
43
+ /** Defaults to the site description. */
44
+ description?: string;
45
+ /** License URL or SPDX identifier. */
46
+ license?: string;
47
+ /** Defaults to the site title. */
48
+ name?: string;
49
+ operatingSystem?: string;
50
+ /** Emitted as an `Offer` when set; `0` marks the software free. */
51
+ price?: number | string;
52
+ priceCurrency: string;
53
+ /** Package registry, repository, and profile URLs for the product. */
54
+ sameAs: string[];
55
+ }
56
+
57
+ /** The site-level identity nodes, from `seo.organization`/`seo.software`. */
58
+ export interface StructuredDataIdentity {
59
+ organization?: OrganizationIdentity;
60
+ software?: SoftwareIdentity;
61
+ }
62
+
7
63
  /** Inputs for a page's JSON-LD, all known at render time in RootLayout. */
8
64
  export interface StructuredDataInput {
9
65
  siteName: string;
@@ -24,6 +80,12 @@ export interface StructuredDataInput {
24
80
  /** BCP-47 language tag for `inLanguage`; defaults to `en`. */
25
81
  locale?: string;
26
82
  breadcrumbs: Crumb[];
83
+ /**
84
+ * Site identity (`seo.organization`, `seo.software`). Both nodes need an
85
+ * absolute `@id`, so they are emitted only when `siteUrl` is set — like the
86
+ * WebSite node.
87
+ */
88
+ identity?: StructuredDataIdentity | null;
27
89
  }
28
90
 
29
91
  /** schema.org `@type` for each content type; defaults to TechArticle. */
@@ -66,11 +128,193 @@ export const toIso = (value: DateInput | undefined): string | undefined => {
66
128
  return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
67
129
  };
68
130
 
131
+ /** Copy the defined string entries of `source` onto a fresh node. */
132
+ const definedStrings = (
133
+ source: Record<string, string | undefined>
134
+ ): JsonLdNode => {
135
+ const node: JsonLdNode = {};
136
+ for (const [key, value] of Object.entries(source)) {
137
+ if (value) {
138
+ node[key] = value;
139
+ }
140
+ }
141
+ return node;
142
+ };
143
+
69
144
  /**
70
- * Build a schema.org JSON-LD `@graph` for a page: site identity, the page as an
71
- * article, and its breadcrumb trail. Returns null when there is nothing useful
72
- * to emit (e.g. the homepage without a configured site). URLs are absolute when
73
- * `siteUrl` is set, otherwise route-relative.
145
+ * The `Organization` node: name and URL (defaulting to the site's), logo, the
146
+ * `sameAs` profiles, a `ContactPoint` when there is a way to make contact,
147
+ * and a `PostalAddress` when any part of one is given — the fields agents
148
+ * check to verify a business before recommending it.
149
+ */
150
+ const organizationNode = (
151
+ organization: OrganizationIdentity,
152
+ context: {
153
+ absolutize: (path: string) => string;
154
+ id: string;
155
+ rootUrl: string;
156
+ siteName: string;
157
+ }
158
+ ): JsonLdNode => {
159
+ const node: JsonLdNode = {
160
+ "@id": context.id,
161
+ "@type": "Organization",
162
+ name: organization.name ?? context.siteName,
163
+ url: organization.url ?? context.rootUrl,
164
+ };
165
+ if (organization.logo) {
166
+ node.logo = context.absolutize(organization.logo);
167
+ }
168
+ if (organization.email) {
169
+ node.email = organization.email;
170
+ }
171
+ if (organization.telephone) {
172
+ node.telephone = organization.telephone;
173
+ }
174
+ if (organization.email || organization.telephone) {
175
+ node.contactPoint = {
176
+ "@type": "ContactPoint",
177
+ contactType: organization.contactType,
178
+ ...definedStrings({
179
+ email: organization.email,
180
+ telephone: organization.telephone,
181
+ }),
182
+ };
183
+ }
184
+ const address = organization.address
185
+ ? definedStrings({ ...organization.address })
186
+ : {};
187
+ if (Object.keys(address).length > 0) {
188
+ node.address = { "@type": "PostalAddress", ...address };
189
+ }
190
+ if (organization.sameAs.length > 0) {
191
+ node.sameAs = organization.sameAs;
192
+ }
193
+ return node;
194
+ };
195
+
196
+ /**
197
+ * The homepage `SoftwareApplication` node: the product's identity (name,
198
+ * description, category), an `Offer` when a price is declared (`0` for free
199
+ * software), license, registry/repository profiles, and the organization as
200
+ * its publisher when one is configured.
201
+ */
202
+ const softwareNode = (
203
+ software: SoftwareIdentity,
204
+ context: {
205
+ description?: string;
206
+ id: string;
207
+ organizationId: string | null;
208
+ rootUrl: string;
209
+ siteName: string;
210
+ }
211
+ ): JsonLdNode => {
212
+ const node: JsonLdNode = {
213
+ "@id": context.id,
214
+ "@type": "SoftwareApplication",
215
+ applicationCategory: software.applicationCategory,
216
+ name: software.name ?? context.siteName,
217
+ url: context.rootUrl,
218
+ };
219
+ const description = software.description ?? context.description;
220
+ if (description) {
221
+ node.description = description;
222
+ }
223
+ if (software.operatingSystem) {
224
+ node.operatingSystem = software.operatingSystem;
225
+ }
226
+ if (software.price !== undefined) {
227
+ node.offers = {
228
+ "@type": "Offer",
229
+ price: String(software.price),
230
+ priceCurrency: software.priceCurrency,
231
+ };
232
+ }
233
+ if (software.license) {
234
+ node.license = software.license;
235
+ }
236
+ if (software.sameAs.length > 0) {
237
+ node.sameAs = software.sameAs;
238
+ }
239
+ if (context.organizationId) {
240
+ node.publisher = { "@id": context.organizationId };
241
+ }
242
+ return node;
243
+ };
244
+
245
+ /** The page as an article node (`BlogPosting`/`TechArticle`). */
246
+ const articleNode = (
247
+ input: StructuredDataInput,
248
+ context: {
249
+ base: string | null;
250
+ organizationId: string | null;
251
+ pageUrl: string;
252
+ }
253
+ ): JsonLdNode => {
254
+ const pageType = input.pageType ?? "";
255
+ const node: JsonLdNode = {
256
+ "@id": `${context.pageUrl}#page`,
257
+ "@type": isArticleType(pageType) ? ARTICLE_TYPES[pageType] : "TechArticle",
258
+ headline: input.title,
259
+ inLanguage: input.locale || "en",
260
+ name: input.title,
261
+ url: context.pageUrl,
262
+ };
263
+ if (input.description) {
264
+ node.description = input.description;
265
+ }
266
+ const published = toIso(input.published);
267
+ if (published) {
268
+ node.datePublished = published;
269
+ }
270
+ const modified = toIso(input.modified);
271
+ if (modified) {
272
+ node.dateModified = modified;
273
+ }
274
+ if (context.base) {
275
+ node.isPartOf = { "@id": `${context.base}#website` };
276
+ }
277
+ if (context.organizationId) {
278
+ node.publisher = { "@id": context.organizationId };
279
+ }
280
+ return node;
281
+ };
282
+
283
+ /**
284
+ * The breadcrumb trail, or null when it is too short to be one. Google
285
+ * requires `item` on every ListItem except the last; sidebar groups without
286
+ * an index page produce route-less crumbs, so those are dropped (positions
287
+ * renumbered) rather than emitted as invalid link-less items.
288
+ */
289
+ const breadcrumbNode = (
290
+ breadcrumbs: Crumb[],
291
+ base: string | null,
292
+ deployBase: string
293
+ ): JsonLdNode | null => {
294
+ const linked = breadcrumbs.filter(
295
+ (crumb): crumb is Required<Crumb> => typeof crumb.route === "string"
296
+ );
297
+ if (linked.length <= 1) {
298
+ return null;
299
+ }
300
+ return {
301
+ "@type": "BreadcrumbList",
302
+ itemListElement: linked.map((crumb, index) => ({
303
+ "@type": "ListItem",
304
+ item: absolute(base, withBasePath(deployBase, crumb.route)),
305
+ name: crumb.label,
306
+ position: index + 1,
307
+ })),
308
+ };
309
+ };
310
+
311
+ /**
312
+ * Build a schema.org JSON-LD `@graph` for a page: site identity (the WebSite,
313
+ * plus the configured Organization everywhere and the SoftwareApplication on
314
+ * the homepage), the page as an article, and its breadcrumb trail. Returns
315
+ * null when there is nothing useful to emit (e.g. the homepage without a
316
+ * configured site). URLs are absolute when `siteUrl` is set, otherwise
317
+ * route-relative.
74
318
  */
75
319
  export const buildStructuredData = (
76
320
  input: StructuredDataInput
@@ -83,61 +327,59 @@ export const buildStructuredData = (
83
327
  const rootUrl = absolute(base, deployBase);
84
328
  const graph: JsonLdNode[] = [];
85
329
 
330
+ // Identity nodes carry absolute `@id`s, so they exist only with a site —
331
+ // the same rule as the WebSite node they attach to.
332
+ const organization = base ? input.identity?.organization : undefined;
333
+ const software = base ? input.identity?.software : undefined;
334
+ const organizationId = organization ? `${base}#organization` : null;
335
+
86
336
  if (base) {
87
- graph.push({
337
+ const website: JsonLdNode = {
88
338
  "@id": `${base}#website`,
89
339
  "@type": "WebSite",
90
340
  name: input.siteName,
91
341
  url: rootUrl,
92
- });
93
- }
94
-
95
- // The homepage is fully described by the WebSite node; deeper pages get an
96
- // article node plus a breadcrumb trail.
97
- if (input.route !== "/") {
98
- const pageType = input.pageType ?? "";
99
- const node: JsonLdNode = {
100
- "@id": `${pageUrl}#page`,
101
- "@type": isArticleType(pageType)
102
- ? ARTICLE_TYPES[pageType]
103
- : "TechArticle",
104
- headline: input.title,
105
- inLanguage: input.locale || "en",
106
- name: input.title,
107
- url: pageUrl,
108
342
  };
109
- if (input.description) {
110
- node.description = input.description;
343
+ if (organizationId) {
344
+ website.publisher = { "@id": organizationId };
111
345
  }
112
- const published = toIso(input.published);
113
- if (published) {
114
- node.datePublished = published;
115
- }
116
- const modified = toIso(input.modified);
117
- if (modified) {
118
- node.dateModified = modified;
119
- }
120
- if (base) {
121
- node.isPartOf = { "@id": `${base}#website` };
122
- }
123
- graph.push(node);
124
-
125
- // Google requires `item` on every ListItem except the last; sidebar groups
126
- // without an index page produce route-less crumbs, so those are dropped
127
- // (positions renumbered) rather than emitted as invalid link-less items.
128
- const linked = input.breadcrumbs.filter(
129
- (crumb): crumb is Required<Crumb> => typeof crumb.route === "string"
346
+ graph.push(website);
347
+ }
348
+ if (organization && organizationId) {
349
+ graph.push(
350
+ organizationNode(organization, {
351
+ // A root-relative logo lives under the deployment base like any
352
+ // other site asset; an absolute URL passes through.
353
+ absolutize: (path) =>
354
+ path.startsWith("/")
355
+ ? absolute(base, withBasePath(deployBase, path))
356
+ : path,
357
+ id: organizationId,
358
+ rootUrl,
359
+ siteName: input.siteName,
360
+ })
130
361
  );
131
- if (linked.length > 1) {
132
- graph.push({
133
- "@type": "BreadcrumbList",
134
- itemListElement: linked.map((crumb, index) => ({
135
- "@type": "ListItem",
136
- item: absolute(base, withBasePath(deployBase, crumb.route)),
137
- name: crumb.label,
138
- position: index + 1,
139
- })),
140
- });
362
+ }
363
+
364
+ // The homepage is described by the WebSite node (and the product, when one
365
+ // is configured); deeper pages get an article node plus a breadcrumb trail.
366
+ if (input.route === "/") {
367
+ if (software && base) {
368
+ graph.push(
369
+ softwareNode(software, {
370
+ description: input.description,
371
+ id: `${base}#software`,
372
+ organizationId,
373
+ rootUrl,
374
+ siteName: input.siteName,
375
+ })
376
+ );
377
+ }
378
+ } else {
379
+ graph.push(articleNode(input, { base, organizationId, pageUrl }));
380
+ const breadcrumbs = breadcrumbNode(input.breadcrumbs, base, deployBase);
381
+ if (breadcrumbs) {
382
+ graph.push(breadcrumbs);
141
383
  }
142
384
  }
143
385
 
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Vertical padding of a highlighted code block, shared by the theme (which
3
+ * emits it as CSS) and `<Component>` (which estimates a source pane's SSR
4
+ * height from it) so the two cannot drift.
5
+ */
6
+
7
+ /** Top and bottom inset of a plain prose block with no chrome, in rem. */
8
+ export const CODE_PADDING_BLOCK_REM = 1;
9
+
10
+ /**
11
+ * Top inset of a flush block — inside tabs or a `not-prose` component, or an
12
+ * untitled block with no language bar — where the layout's copy button is
13
+ * absolutely positioned over the first line: `top-2.5` plus a 1.875rem button
14
+ * lands at 2.5rem, so the first line starts there.
15
+ */
16
+ export const FLUSH_CODE_PADDING_TOP_REM = 2.5;
@@ -1,3 +1,8 @@
1
+ import {
2
+ CODE_PADDING_BLOCK_REM,
3
+ FLUSH_CODE_PADDING_TOP_REM,
4
+ } from "./code-block-padding.ts";
5
+
1
6
  interface TailwindEntryOptions {
2
7
  /**
3
8
  * Globs to scan for utility classes. Typically the Blume package source and
@@ -37,7 +42,10 @@ const TOKEN_DEFAULTS = `:root {
37
42
  --blume-background-image-size: cover;
38
43
  --blume-foreground: oklch(0.145 0 0);
39
44
  --blume-muted: oklch(0.965 0 0);
40
- --blume-muted-foreground: oklch(0.54 0 0);
45
+ /* 5.28:1 on the page background. The headroom over 4.5:1 is the point: muted
46
+ text is 14px body copy routinely set on a tinted surface (callouts, badges,
47
+ panels), and each tint costs a few tenths. */
48
+ --blume-muted-foreground: oklch(0.53 0 0);
41
49
  --blume-border: oklch(0.88 0.006 260 / 0.72);
42
50
  --blume-accent: oklch(0.145 0 0);
43
51
  --blume-accent-foreground: oklch(1 0 0);
@@ -252,6 +260,20 @@ ${THEME_MAPPING}
252
260
  color: var(--blume-muted-foreground);
253
261
  font-size: 0.875rem;
254
262
  line-height: 1.7;
263
+ /* An unbreakable run — an API permission, a broker list, a bare URL used as
264
+ its own link text, an OpenAPI operation title of the form
265
+ \`METHOD /very/long/{path}\`, a module-qualified name in a generated
266
+ reference — paints past the content column, and nothing between the
267
+ paragraph and the viewport clips it, so it lands in the document's scroll
268
+ width and drags the page sideways on a phone. Inherited, so headings and
269
+ inline code are covered too; the only re-declarations below are the
270
+ opt-in code-wrap mode and Twoslash, which want different values.
271
+ \`break-word\` and not \`anywhere\`: only \`anywhere\` reduces min-content,
272
+ which is what sizes \`table-layout: auto\` columns, so it would resize
273
+ every table on the site. The flip side is that a token in a table cell
274
+ still never breaks — the column grows to fit it — so wide tables rely on
275
+ the .blume-table-scroll wrapper, not on this. */
276
+ overflow-wrap: break-word;
255
277
  }
256
278
 
257
279
  /* No letter-spacing here: prose headings inherit the base h1-h6 rule's
@@ -260,13 +282,6 @@ ${THEME_MAPPING}
260
282
  font-weight: 500;
261
283
  }
262
284
 
263
- /* A heading can carry one long unbreakable token — an OpenAPI operation's title
264
- is \`METHOD /very/long/{path}\` when the spec sets no summary — which would run
265
- off the content column. Break it across lines instead of overflowing. */
266
- .prose :where(h1, h2, h3, h4, h5, h6) {
267
- overflow-wrap: break-word;
268
- }
269
-
270
285
  .prose :where(h1) {
271
286
  font-size: 3rem;
272
287
  line-height: 1.1;
@@ -311,6 +326,18 @@ ${THEME_MAPPING}
311
326
  opacity: 1;
312
327
  }
313
328
 
329
+ /* A [toc]-marked heading exists only for the table of contents: it stays in
330
+ the flow as a zero-height, invisible anchor target so its TOC entry (and any
331
+ deep link) still has somewhere to scroll to, without rendering on the page. */
332
+ .prose :where(.blume-toc-only) {
333
+ border: 0;
334
+ height: 0;
335
+ margin: 0;
336
+ overflow: hidden;
337
+ padding: 0;
338
+ visibility: hidden;
339
+ }
340
+
314
341
  .prose :where(h2:first-child) {
315
342
  border-top: 0;
316
343
  padding-top: 0;
@@ -354,7 +381,7 @@ ${THEME_MAPPING}
354
381
  line-height: 1.55;
355
382
  margin: 1.5rem 0;
356
383
  overflow-x: auto;
357
- padding: 1rem 0;
384
+ padding: ${CODE_PADDING_BLOCK_REM}rem 0;
358
385
  position: relative;
359
386
  }
360
387
 
@@ -534,7 +561,6 @@ blume-diff {
534
561
  never matches (a cell is not its own descendant). */
535
562
  .prose :where(td, th) > code,
536
563
  .prose :where(td, th) :not(pre) > code {
537
- overflow-wrap: break-word;
538
564
  white-space: normal;
539
565
  }
540
566
 
@@ -578,9 +604,37 @@ blume-tabs [data-blume-tab-content] > pre {
578
604
  margin: 0;
579
605
  }
580
606
 
607
+ /* These contexts drop the language bar (\`::before\` is cleared below), so the
608
+ bar's padding goes with it and the block keeps the plain inset. Unscoped on
609
+ purpose: <CodeBlock> wraps its pre in its own \`.prose\` below the component
610
+ chrome, so without this a titled block on a page with no prose column
611
+ (PageLayout) would keep the 3.75rem bar padding under a cleared bar. */
581
612
  blume-tabs pre[data-language],
582
613
  .not-prose pre[data-language] {
583
- padding-top: 1rem;
614
+ padding-top: ${CODE_PADDING_BLOCK_REM}rem;
615
+ }
616
+
617
+ /* Room for the copy button. The docs layout injects it into every \`.prose pre\`
618
+ — absolutely positioned against the pre's border box, 1.875rem tall, at
619
+ \`top-2.5\` in the flush contexts above and \`top-2\` elsewhere — and it is
620
+ unconditional, so wherever no language bar exists to hold it, it painted
621
+ over the first line of code: any line long enough to reach the button's
622
+ strip (a curl invocation, an install command, an import path) went under
623
+ it. Keyed on the attribute the docs layout stamps on <body>, so the strip
624
+ only appears where the injector runs (PageLayout pages run none), and on
625
+ \`.astro-code\` so the playground's response pre — created after the
626
+ injector ran, never given a button — keeps its plain inset. The first
627
+ selector is the injector's own flush predicate; the second covers the
628
+ remaining bar-less Shiki case, an untitled <CodeBlock> in plain prose, from
629
+ first paint. The third covers a raw \`<pre><code>\` written in prose — no
630
+ \`.astro-code\`, no language bar, but the injector gives it a button all
631
+ the same — keyed on that injected button, which is exactly what separates
632
+ it from the playground's pre. API panel blocks match, but their own
633
+ !important padding wins and they hide the injected button. */
634
+ [data-blume-code-copy] :is(blume-tabs, .not-prose) pre.astro-code,
635
+ [data-blume-code-copy] .prose pre.astro-code:not([data-language]),
636
+ [data-blume-code-copy] .prose pre:not([data-language]):has(> [data-blume-copy]) {
637
+ padding-top: ${FLUSH_CODE_PADDING_TOP_REM}rem;
584
638
  }
585
639
 
586
640
  blume-tabs pre[data-language]::before,