blume 1.5.2 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +85 -0
- package/dist/cli/index.js +3639 -1377
- package/dist/cli/index.js.map +103 -91
- package/dist/types/ai/component-markdown.d.ts +79 -0
- package/dist/types/components/layout/nav-utils.d.ts +60 -0
- package/dist/types/core/base-path.d.ts +9 -0
- package/dist/types/core/config-input.d.ts +206 -4
- package/dist/types/core/config.d.ts +6 -4
- package/dist/types/core/data.d.ts +23 -1
- package/dist/types/core/github.d.ts +35 -0
- package/dist/types/core/i18n-ui.d.ts +8 -0
- package/dist/types/core/navigation.d.ts +69 -0
- package/dist/types/core/schema.d.ts +117 -1
- package/dist/types/core/sources/types.d.ts +31 -6
- package/dist/types/core/types.d.ts +23 -2
- package/dist/types/markdown/features.d.ts +21 -0
- package/dist/types/openapi/references.d.ts +21 -1
- package/dist/types/seo/jsonld.d.ts +105 -0
- package/dist/types/theme/fonts.d.ts +34 -4
- package/docs/_snippets/include-demo.mdx +7 -0
- package/docs/advanced/api-reference.mdx +3 -3
- package/docs/advanced/custom-pages.mdx +1 -1
- package/docs/advanced/graphql.mdx +84 -0
- package/docs/advanced/meta.ts +8 -1
- package/docs/configuration/ai.mdx +21 -3
- package/docs/configuration/index.mdx +24 -0
- package/docs/configuration/search.mdx +13 -1
- package/docs/configuration/seo.mdx +27 -0
- package/docs/configuration/theming.mdx +17 -0
- package/docs/content/components.mdx +7 -0
- package/docs/content/includes.mdx +68 -0
- package/docs/content/meta.ts +1 -0
- package/docs/content/navigation.mdx +25 -0
- package/docs/content/sources.mdx +42 -1
- package/docs/content/syntax.mdx +69 -1
- package/docs/content/versioning.mdx +15 -9
- package/docs/reference/cli.mdx +2 -1
- package/package.json +23 -14
- package/skills/blume-migrate/SKILL.md +16 -7
- package/skills/blume-migrate/references/docusaurus.md +5 -3
- package/skills/blume-migrate/references/fumadocs.md +10 -2
- package/skills/blume-migrate/references/mintlify.md +3 -2
- package/skills/blume-migrate/references/nextra.md +2 -2
- package/skills/blume-migrate/references/starlight.md +1 -1
- package/src/ai/agent-readability.ts +2 -1
- package/src/ai/ask-data.ts +2 -1
- package/src/ai/component-markdown.ts +199 -36
- package/src/ai/llms.ts +93 -6
- package/src/ai/markdown.ts +2 -2
- package/src/ai/mcp/discovery.ts +10 -2
- package/src/ai/mcp/server.ts +74 -2
- package/src/astro/generate.ts +183 -116
- package/src/astro/include-hmr.ts +81 -0
- package/src/astro/include-refresh.ts +0 -0
- package/src/astro/index.ts +3 -5
- package/src/astro/templates.ts +125 -76
- package/src/cli/commands/build.ts +84 -15
- package/src/cli/init/questions.ts +1 -0
- package/src/cli/init/scaffold.ts +27 -4
- package/src/components/colors.ts +142 -0
- package/src/components/content/Badge.astro +5 -12
- package/src/components/content/Callout.astro +19 -36
- package/src/components/content/Card.astro +15 -21
- package/src/components/content/Component.astro +10 -1
- package/src/components/content/GithubInfo.astro +28 -9
- package/src/components/content/Tabs.astro +27 -5
- package/src/components/content/github-info.ts +20 -5
- package/src/components/dropdown-dismiss.ts +122 -0
- package/src/components/layout/Fonts.astro +15 -8
- package/src/components/layout/Header.astro +44 -0
- package/src/components/layout/LanguageSwitcher.astro +9 -1
- package/src/components/layout/NavSelector.astro +12 -3
- package/src/components/layout/NavTree.astro +6 -18
- package/src/components/layout/PageActions.astro +29 -8
- package/src/components/layout/PageLayout.astro +10 -1
- package/src/components/layout/ReferenceLayout.astro +6 -1
- package/src/components/layout/RootLayout.astro +46 -15
- package/src/components/layout/Search.astro +36 -4
- package/src/components/layout/TableOfContents.astro +8 -2
- package/src/components/layout/head-scripts.ts +53 -1
- package/src/components/openapi/ApiOverview.astro +13 -3
- package/src/components/openapi/AsyncApiOperation.astro +7 -14
- package/src/components/openapi/GraphqlChip.astro +33 -0
- package/src/components/openapi/GraphqlFieldsTable.astro +111 -0
- package/src/components/openapi/GraphqlOperation.astro +186 -0
- package/src/components/openapi/GraphqlType.astro +154 -0
- package/src/components/openapi/MethodBadge.astro +3 -14
- package/src/components/openapi/Operation.astro +12 -5
- package/src/components/openapi/OperationPanel.astro +43 -0
- package/src/components/openapi/RequestPanel.astro +5 -10
- package/src/components/openapi/Responses.astro +1 -16
- package/src/components/openapi/graphql-helpers.ts +466 -0
- package/src/components/openapi/playground-client.ts +15 -0
- package/src/components/openapi/sample-panels.ts +45 -0
- package/src/components/openapi/snippets.ts +13 -35
- package/src/core/base-path.ts +11 -0
- package/src/core/config-input.ts +209 -2
- package/src/core/config.ts +6 -4
- package/src/core/content-assets.ts +15 -4
- package/src/core/data.ts +18 -2
- package/src/core/diagnostics.ts +8 -0
- package/src/core/frontmatter.ts +20 -8
- package/src/core/github.ts +71 -0
- package/src/core/graph.ts +22 -8
- package/src/core/heading-markers.ts +96 -0
- package/src/core/i18n-ui.ts +11 -0
- package/src/core/includes.ts +632 -0
- package/src/core/last-modified.ts +36 -11
- package/src/core/links.ts +79 -13
- package/src/core/meta.ts +2 -1
- package/src/core/nav-diagnostics.ts +11 -2
- package/src/core/navigation.ts +27 -6
- package/src/core/project-graph.ts +61 -9
- package/src/core/schema.ts +226 -35
- package/src/core/server-features.ts +5 -9
- package/src/core/sources/github-releases.ts +2 -2
- package/src/core/sources/normalize.ts +502 -115
- package/src/core/sources/notion.ts +43 -8
- package/src/core/sources/obsidian.ts +1038 -0
- package/src/core/sources/read.ts +36 -1
- package/src/core/sources/resolve.ts +34 -1
- package/src/core/sources/types.ts +28 -6
- package/src/core/sources/watch.ts +12 -8
- package/src/core/tsconfig-aliases.ts +48 -35
- package/src/core/types.ts +25 -2
- package/src/core/ui-packs/ar.ts +1 -0
- package/src/core/ui-packs/bg.ts +2 -0
- package/src/core/ui-packs/bn.ts +1 -0
- package/src/core/ui-packs/ca.ts +2 -0
- package/src/core/ui-packs/cs.ts +1 -0
- package/src/core/ui-packs/da.ts +1 -0
- package/src/core/ui-packs/de.ts +2 -0
- package/src/core/ui-packs/el.ts +2 -0
- package/src/core/ui-packs/es.ts +2 -0
- package/src/core/ui-packs/fa.ts +1 -0
- package/src/core/ui-packs/fi.ts +1 -0
- package/src/core/ui-packs/fr.ts +2 -0
- package/src/core/ui-packs/he.ts +1 -0
- package/src/core/ui-packs/hi.ts +1 -0
- package/src/core/ui-packs/hr.ts +2 -0
- package/src/core/ui-packs/hu.ts +2 -0
- package/src/core/ui-packs/id.ts +2 -0
- package/src/core/ui-packs/it.ts +1 -0
- package/src/core/ui-packs/ja.ts +2 -0
- package/src/core/ui-packs/ko.ts +2 -0
- package/src/core/ui-packs/nl.ts +2 -0
- package/src/core/ui-packs/no.ts +2 -0
- package/src/core/ui-packs/pl.ts +2 -0
- package/src/core/ui-packs/pt-br.ts +2 -0
- package/src/core/ui-packs/pt.ts +2 -0
- package/src/core/ui-packs/ro.ts +2 -0
- package/src/core/ui-packs/ru.ts +2 -0
- package/src/core/ui-packs/sk.ts +1 -0
- package/src/core/ui-packs/sr.ts +1 -0
- package/src/core/ui-packs/sv.ts +2 -0
- package/src/core/ui-packs/th.ts +1 -0
- package/src/core/ui-packs/tr.ts +2 -0
- package/src/core/ui-packs/uk.ts +2 -0
- package/src/core/ui-packs/vi.ts +1 -0
- package/src/core/ui-packs/zh-tw.ts +1 -0
- package/src/core/ui-packs/zh.ts +1 -0
- package/src/core/version-cut.ts +21 -3
- package/src/core/yaml.ts +26 -0
- package/src/deploy/function-bundle.ts +251 -0
- package/src/eval/schema.ts +3 -1
- package/src/markdown/code-title.ts +22 -16
- package/src/markdown/features.ts +21 -0
- package/src/markdown/fence-meta.ts +50 -0
- package/src/markdown/heading-anchors.ts +198 -37
- package/src/markdown/include.ts +247 -0
- package/src/markdown/index.ts +43 -34
- package/src/markdown/language-icon.ts +2 -2
- package/src/markdown/mdast.ts +7 -3
- package/src/markdown/ts2js.ts +264 -0
- package/src/openapi/asyncapi.ts +4 -1
- package/src/openapi/graphql-build.ts +293 -0
- package/src/openapi/graphql.ts +212 -0
- package/src/openapi/model.ts +38 -5
- package/src/openapi/parse.ts +34 -0
- package/src/openapi/proxy.ts +30 -5
- package/src/openapi/references.ts +89 -13
- package/src/openapi/render-mdx.ts +48 -8
- package/src/openapi/scalar.ts +5 -12
- package/src/openapi/source.ts +91 -23
- package/src/registry/eject.ts +11 -0
- package/src/search/documents.ts +229 -37
- package/src/search/orama-index.ts +9 -5
- package/src/seo/jsonld.ts +293 -51
- package/src/theme/code-block-padding.ts +16 -0
- package/src/theme/entry.ts +65 -11
- package/src/theme/fonts.ts +189 -16
- package/src/translate/prompts.ts +2 -0
- package/src/translate/run.ts +7 -0
- package/src/translate/work-list.ts +0 -0
package/src/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
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
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
|
-
|
|
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 (
|
|
110
|
-
|
|
343
|
+
if (organizationId) {
|
|
344
|
+
website.publisher = { "@id": organizationId };
|
|
111
345
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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;
|
package/src/theme/entry.ts
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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:
|
|
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,
|