blume 1.6.3 → 1.6.5
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 +38 -0
- package/dist/cli/index.js +308 -50
- package/dist/cli/index.js.map +29 -24
- package/dist/types/ai/component-markdown.d.ts +14 -0
- package/dist/types/core/config-input.d.ts +10 -0
- package/dist/types/core/schema.d.ts +4 -1
- package/docs/01-quickstart.mdx +2 -2
- package/docs/02-deployment.mdx +5 -5
- package/docs/{07-faq.mdx → 08-faq.mdx} +7 -7
- package/docs/advanced/blog.mdx +3 -3
- package/docs/advanced/changelog.mdx +2 -2
- package/docs/advanced/custom-pages.mdx +4 -4
- package/docs/advanced/meta.ts +1 -1
- package/docs/configuration/analytics.mdx +21 -2
- package/docs/configuration/ask-ai.mdx +179 -0
- package/docs/configuration/index.mdx +8 -7
- package/docs/configuration/meta.ts +1 -2
- package/docs/configuration/search.mdx +1 -1
- package/docs/configuration/theming.mdx +1 -1
- package/docs/content/i18n.mdx +7 -1
- package/docs/content/index.mdx +1 -1
- package/docs/content/navigation.mdx +2 -2
- package/docs/content/syntax.mdx +2 -2
- package/docs/discoverability/agent-discovery.mdx +196 -0
- package/docs/discoverability/index.mdx +48 -0
- package/docs/discoverability/json-api.mdx +58 -0
- package/docs/discoverability/llms-txt.mdx +68 -0
- package/docs/discoverability/markdown.mdx +76 -0
- package/docs/discoverability/mcp.mdx +64 -0
- package/docs/discoverability/meta.ts +18 -0
- package/docs/discoverability/metadata.mdx +82 -0
- package/docs/discoverability/open-graph.mdx +113 -0
- package/docs/discoverability/rss.mdx +24 -0
- package/docs/discoverability/sitemap-and-robots.mdx +95 -0
- package/docs/discoverability/structured-data.mdx +51 -0
- package/docs/index.mdx +5 -5
- package/docs/reference/eval.mdx +1 -1
- package/docs/reference/meta.ts +1 -1
- package/docs/reference/translate.mdx +1 -0
- package/package.json +27 -27
- package/src/ai/component-markdown.ts +17 -2
- package/src/ai/llms.ts +3 -10
- package/src/ai/markdown.ts +3 -10
- package/src/ai/openapi-components.ts +123 -0
- package/src/ai/serializers.ts +24 -0
- package/src/astro/generate.ts +5 -6
- package/src/astro/templates.ts +42 -12
- package/src/audit/checks/links.ts +1 -8
- package/src/audit/checks/llms.ts +5 -4
- package/src/audit/redirects.ts +4 -3
- package/src/audit/run.ts +6 -8
- package/src/audit/url.ts +33 -0
- package/src/cli/commands/validate.ts +1 -0
- package/src/components/content/Component.astro +60 -59
- package/src/components/content/example-pane.ts +6 -0
- package/src/components/content/mermaid-element.ts +8 -0
- package/src/components/layout/Analytics.astro +20 -1
- package/src/components/layout/LocaleLinks.astro +42 -0
- package/src/components/layout/PageLayout.astro +5 -3
- package/src/components/layout/ReferenceLayout.astro +6 -0
- package/src/components/layout/RootLayout.astro +6 -3
- package/src/components/layout/analytics-client.ts +2 -1
- package/src/components/layout/search-locale.ts +13 -0
- package/src/components/openapi/ApiOverview.astro +7 -39
- package/src/components/openapi/ApiTagOperations.astro +2 -1
- package/src/components/openapi/AsyncApiOperation.astro +8 -5
- package/src/components/openapi/Authorization.astro +4 -6
- package/src/components/openapi/Bindings.astro +2 -2
- package/src/components/openapi/Description.astro +109 -0
- package/src/components/openapi/GraphqlFieldsTable.astro +5 -7
- package/src/components/openapi/GraphqlOperation.astro +7 -5
- package/src/components/openapi/GraphqlType.astro +4 -6
- package/src/components/openapi/Operation.astro +3 -2
- package/src/components/openapi/ParametersTable.astro +5 -7
- package/src/components/openapi/RequestBody.astro +2 -4
- package/src/components/openapi/Responses.astro +4 -3
- package/src/components/openapi/SchemaProperty.astro +11 -6
- package/src/components/openapi/description.ts +91 -0
- package/src/core/config-input.ts +10 -0
- package/src/core/i18n.ts +13 -2
- package/src/core/links.ts +33 -1
- package/src/core/locale-links.ts +163 -0
- package/src/core/schema.ts +8 -0
- package/src/core/sources/normalize.ts +57 -7
- package/src/markdown/package-commands.ts +27 -3
- package/src/openapi/graphql.ts +29 -0
- package/src/openapi/model.ts +69 -0
- package/src/openapi/render-mdx.ts +3 -2
- package/src/openapi/signature.ts +18 -0
- package/src/search/documents.ts +4 -9
- package/src/theme/entry.ts +28 -4
- package/src/translate/anchors.ts +91 -0
- package/src/translate/validate.ts +8 -3
- package/docs/configuration/ai.mdx +0 -613
- package/docs/configuration/seo.mdx +0 -364
|
@@ -3,7 +3,7 @@ import VercelAnalytics from "@vercel/analytics/astro";
|
|
|
3
3
|
|
|
4
4
|
// Analytics scripts injected into <head>. PostHog and custom providers use the
|
|
5
5
|
// inline-snippet house style (banner/theme/JSON-LD); Vercel uses its official
|
|
6
|
-
// Astro component. Loads ONLY in production builds so `blume dev` stays clean
|
|
6
|
+
// Astro component; Cloudflare is its beacon script tag. Loads ONLY in production builds so `blume dev` stays clean
|
|
7
7
|
// and local traffic never reaches your analytics.
|
|
8
8
|
interface AnalyticsScript {
|
|
9
9
|
attributes?: Record<string, string>;
|
|
@@ -14,6 +14,7 @@ interface AnalyticsScript {
|
|
|
14
14
|
|
|
15
15
|
interface Props {
|
|
16
16
|
analytics?: {
|
|
17
|
+
cloudflare?: { token: string };
|
|
17
18
|
posthog?: { host?: string; key: string };
|
|
18
19
|
scripts?: AnalyticsScript[];
|
|
19
20
|
vercel?: boolean;
|
|
@@ -45,6 +46,14 @@ const posthogSnippet = posthog
|
|
|
45
46
|
? `${POSTHOG_LOADER}posthog.init(${JSON.stringify(posthog.key)},{api_host:${JSON.stringify(posthog.host ?? "https://us.i.posthog.com")}});${POSTHOG_SPA_PAGEVIEWS}`
|
|
46
47
|
: null;
|
|
47
48
|
|
|
49
|
+
// Cloudflare Web Analytics (manual setup): the beacon script keyed by the
|
|
50
|
+
// site token, exactly as the dashboard's snippet renders it. It tracks
|
|
51
|
+
// history changes itself, so client-router navigations need no extra hook.
|
|
52
|
+
const cloudflareBeacon =
|
|
53
|
+
enabled && analytics?.cloudflare
|
|
54
|
+
? JSON.stringify({ token: analytics.cloudflare.token })
|
|
55
|
+
: null;
|
|
56
|
+
|
|
48
57
|
// Custom scripts: any other provider. Each is either external (`src`) or inline
|
|
49
58
|
// (`content`). Explicit fields win over spread `attributes`.
|
|
50
59
|
const customScripts = enabled ? (analytics?.scripts ?? []) : [];
|
|
@@ -62,6 +71,16 @@ const inlineScripts = customScripts
|
|
|
62
71
|
---
|
|
63
72
|
|
|
64
73
|
{vercelEnabled && <VercelAnalytics />}
|
|
74
|
+
{
|
|
75
|
+
cloudflareBeacon && (
|
|
76
|
+
<script
|
|
77
|
+
is:inline
|
|
78
|
+
defer
|
|
79
|
+
src="https://static.cloudflareinsights.com/beacon.min.js"
|
|
80
|
+
data-cf-beacon={cloudflareBeacon}
|
|
81
|
+
/>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
65
84
|
{posthogSnippet && <script is:inline set:html={posthogSnippet} />}
|
|
66
85
|
{externalScripts.map((attrs) => <script is:inline {...attrs} />)}
|
|
67
86
|
{
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
import data from "blume:data";
|
|
3
|
+
|
|
4
|
+
import { normalizeBasePath } from "../../core/base-path.ts";
|
|
5
|
+
import { localePrefix } from "../../core/i18n.ts";
|
|
6
|
+
import {
|
|
7
|
+
localizeContentLinks,
|
|
8
|
+
localizeHref,
|
|
9
|
+
routeSetFor,
|
|
10
|
+
} from "../../core/locale-links.ts";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Keep a translated page's content links inside the reader's locale. Content
|
|
14
|
+
* is compiled once per file, so its `<a href>`s point at the default locale's
|
|
15
|
+
* routes; rendered under `/fr/…` (a real translation, a fallback page, or a
|
|
16
|
+
* shared `page.$.mdx`), each root-relative page link moves to `/fr/…` when
|
|
17
|
+
* that route is served, and keeps its authored target otherwise. Default-locale
|
|
18
|
+
* pages (no prefix) and single-locale sites pass the slot through untouched.
|
|
19
|
+
*/
|
|
20
|
+
interface Props {
|
|
21
|
+
/** Locale of the route being rendered — a fallback route's own, not its content's. */
|
|
22
|
+
locale: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const { locale } = Astro.props;
|
|
26
|
+
const i18n = data.config.i18n;
|
|
27
|
+
|
|
28
|
+
const html =
|
|
29
|
+
i18n && localePrefix(locale, i18n) !== ""
|
|
30
|
+
? localizeContentLinks(await Astro.slots.render("default"), (href) =>
|
|
31
|
+
localizeHref(href, {
|
|
32
|
+
basePath: data.config.basePath,
|
|
33
|
+
deployBase: normalizeBasePath(import.meta.env.BASE_URL),
|
|
34
|
+
i18n,
|
|
35
|
+
locale,
|
|
36
|
+
routes: routeSetFor(data.routes),
|
|
37
|
+
})
|
|
38
|
+
)
|
|
39
|
+
: null;
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
{html === null ? <slot /> : <Fragment set:html={html} />}
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
import { buildStructuredData } from "../../seo/jsonld.ts";
|
|
36
36
|
import { normalizeXHandle } from "../../seo/x-handle.ts";
|
|
37
37
|
import { withBase } from "../islands/base-path.ts";
|
|
38
|
+
import { searchLocaleFor } from "./search-locale.ts";
|
|
38
39
|
import "blume:theme";
|
|
39
40
|
import { ClientRouter } from "astro:transitions";
|
|
40
41
|
import Analytics from "./Analytics.astro";
|
|
@@ -160,9 +161,10 @@ const strings = ui ?? EN_UI;
|
|
|
160
161
|
// from a not-yet-regenerated snapshot) still renders instead of coming out
|
|
161
162
|
// blank — the PageActions pattern.
|
|
162
163
|
const navStrings = { ...EN_UI.nav, ...strings.nav };
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
// Scope search to the active language on a multi-locale site. Read from the
|
|
165
|
+
// i18n snapshot, not the switcher list, so pages rendered without one still
|
|
166
|
+
// filter (see `searchLocaleFor`).
|
|
167
|
+
const searchLocale = searchLocaleFor(data.config.i18n, locale);
|
|
166
168
|
const pageTitle = page?.title ?? site.title;
|
|
167
169
|
const description = page?.description ?? site.description;
|
|
168
170
|
const route = page?.route ?? "/";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import "blume:theme";
|
|
3
|
+
import data from "blume:data";
|
|
3
4
|
import type { BlumeFavicon } from "../../core/data.ts";
|
|
4
5
|
import { EN_UI } from "../../core/i18n-ui.ts";
|
|
5
6
|
import type { UIStrings } from "../../core/i18n-ui.ts";
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
THEME_INIT_SCRIPT,
|
|
17
18
|
} from "./head-scripts.ts";
|
|
18
19
|
import Header from "./Header.astro";
|
|
20
|
+
import { searchLocaleFor } from "./search-locale.ts";
|
|
19
21
|
|
|
20
22
|
// A minimal shell for the Scalar API/AsyncAPI reference: Blume's banner + navbar
|
|
21
23
|
// on top, then a full-height region the reference mounts into. Unlike RootLayout
|
|
@@ -46,6 +48,7 @@ interface Props {
|
|
|
46
48
|
key: string;
|
|
47
49
|
} | null;
|
|
48
50
|
analytics?: {
|
|
51
|
+
cloudflare?: { token: string };
|
|
49
52
|
posthog?: { host?: string; key: string };
|
|
50
53
|
scripts?: {
|
|
51
54
|
attributes?: Record<string, string>;
|
|
@@ -91,6 +94,8 @@ const {
|
|
|
91
94
|
} = Astro.props;
|
|
92
95
|
|
|
93
96
|
const strings = ui ?? EN_UI;
|
|
97
|
+
// Scope search to the reference page's language on a multi-locale site.
|
|
98
|
+
const searchLocale = searchLocaleFor(data.config.i18n, locale);
|
|
94
99
|
|
|
95
100
|
const bannerKey = banner?.dismissible ? banner.key : null;
|
|
96
101
|
---
|
|
@@ -126,6 +131,7 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
126
131
|
navigation={navigation}
|
|
127
132
|
route={route}
|
|
128
133
|
searchEnabled={searchEnabled}
|
|
134
|
+
searchLocale={searchLocale}
|
|
129
135
|
site={site}
|
|
130
136
|
/>
|
|
131
137
|
<div
|
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
} from "./nav-utils.ts";
|
|
59
59
|
import NavTree from "./NavTree.astro";
|
|
60
60
|
import { resolveSlot } from "./overrides.ts";
|
|
61
|
+
import { searchLocaleFor } from "./search-locale.ts";
|
|
61
62
|
import PageActions from "./PageActions.astro";
|
|
62
63
|
import PageFeedback from "./PageFeedback.astro";
|
|
63
64
|
import Pagination from "./Pagination.astro";
|
|
@@ -83,6 +84,7 @@ interface Props {
|
|
|
83
84
|
key: string;
|
|
84
85
|
} | null;
|
|
85
86
|
analytics?: {
|
|
87
|
+
cloudflare?: { token: string };
|
|
86
88
|
posthog?: { host?: string; key: string };
|
|
87
89
|
scripts?: {
|
|
88
90
|
attributes?: Record<string, string>;
|
|
@@ -289,9 +291,10 @@ const strings = ui ?? EN_UI;
|
|
|
289
291
|
const navStrings = { ...EN_UI.nav, ...strings.nav };
|
|
290
292
|
const actionStrings = { ...EN_UI.actions, ...strings.actions };
|
|
291
293
|
const contentStrings = { ...EN_UI.content, ...strings.content };
|
|
292
|
-
//
|
|
293
|
-
|
|
294
|
-
|
|
294
|
+
// Scope search to the active language on a multi-locale site. Read from the
|
|
295
|
+
// i18n snapshot, not the switcher list, so pages rendered without one still
|
|
296
|
+
// filter (see `searchLocaleFor`).
|
|
297
|
+
const searchLocale = searchLocaleFor(data.config.i18n, locale);
|
|
295
298
|
// TOC entries: the configured heading range, or none when the TOC is disabled
|
|
296
299
|
// (an empty list makes TableOfContents render nothing).
|
|
297
300
|
// API operation pages own a two-column body (docs + request panel) and their own
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Send a custom analytics event to every analytics platform configured in
|
|
3
3
|
* `blume.config.ts`. Mirrors the providers wired by `Analytics.astro`: Vercel
|
|
4
|
-
* Web Analytics and PostHog are first-class
|
|
4
|
+
* Web Analytics and PostHog are first-class (Cloudflare Web Analytics is too,
|
|
5
|
+
* but has no custom-event API to forward to); any other provider added through
|
|
5
6
|
* `analytics.scripts` is reached via best-effort global detection or the
|
|
6
7
|
* `blume:track` CustomEvent, which fires unconditionally so a project can bridge
|
|
7
8
|
* the event to anything. Every call no-ops cleanly when a provider isn't present
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The locale the search dialog should scope to, or `undefined` when scoping
|
|
3
|
+
* is pointless. Derived from the resolved i18n settings rather than from the
|
|
4
|
+
* header's language-switcher entries: the switcher list is only assembled by
|
|
5
|
+
* the content catch-all, so pages rendered without one (custom pages built on
|
|
6
|
+
* `PageLayout`, the changelog index, the 404 page, the API reference shell)
|
|
7
|
+
* would otherwise search every language while showing no "All languages"
|
|
8
|
+
* toggle. A single-locale site (or one without i18n) has nothing to scope.
|
|
9
|
+
*/
|
|
10
|
+
export const searchLocaleFor = (
|
|
11
|
+
i18n: { locales: { code: string }[] } | null,
|
|
12
|
+
locale: string
|
|
13
|
+
): string | undefined => (i18n && i18n.locales.length > 1 ? locale : undefined);
|
|
@@ -1,54 +1,22 @@
|
|
|
1
1
|
---
|
|
2
2
|
import specs from "blume:openapi";
|
|
3
3
|
|
|
4
|
-
import
|
|
4
|
+
import { specAddresses, specOf } from "../../openapi/model.ts";
|
|
5
5
|
|
|
6
6
|
// The spec-level metadata block (version + base URLs) at the top of an API
|
|
7
7
|
// overview page. The tag sections that follow are emitted by `overviewMdx` as
|
|
8
8
|
// markdown headings plus `<ApiTagOperations>` lists, so they land in the
|
|
9
|
-
// table of contents.
|
|
9
|
+
// table of contents. The agent surfaces downlevel this block from the same
|
|
10
|
+
// `specAddresses`, so the `.md` page lists what this one shows.
|
|
10
11
|
interface Props {
|
|
11
12
|
source: string;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
const { source } = Astro.props;
|
|
15
|
-
const spec = specs
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
// live endpoint stands in. All flatten into one list of address chips.
|
|
20
|
-
const addresses: string[] = [];
|
|
21
|
-
if (spec?.kind === "graphql") {
|
|
22
|
-
if (spec.endpoint) {
|
|
23
|
-
addresses.push(spec.endpoint);
|
|
24
|
-
}
|
|
25
|
-
} else if (spec?.kind === "asyncapi") {
|
|
26
|
-
const servers = (spec.document as AsyncApiDocument).servers ?? {};
|
|
27
|
-
for (const server of Object.values(servers)) {
|
|
28
|
-
if (server?.host) {
|
|
29
|
-
addresses.push(
|
|
30
|
-
`${server.protocol ? `${server.protocol}://` : ""}${server.host}${server.pathname ?? ""}`
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
} else {
|
|
35
|
-
// Hand-written specs sometimes declare `servers` as a bare object; degrade
|
|
36
|
-
// to no address chips instead of throwing mid-build.
|
|
37
|
-
const declared = ((spec?.document ?? {}) as { servers?: { url?: string }[] })
|
|
38
|
-
.servers;
|
|
39
|
-
const servers = Array.isArray(declared) ? declared : [];
|
|
40
|
-
for (const server of servers) {
|
|
41
|
-
if (server.url) {
|
|
42
|
-
addresses.push(server.url);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
let addressLabel = "Base URL";
|
|
47
|
-
if (spec?.kind === "asyncapi") {
|
|
48
|
-
addressLabel = "Servers";
|
|
49
|
-
} else if (spec?.kind === "graphql") {
|
|
50
|
-
addressLabel = "Endpoint";
|
|
51
|
-
}
|
|
16
|
+
const spec = specOf(specs, source);
|
|
17
|
+
const { addresses, label: addressLabel } = spec
|
|
18
|
+
? specAddresses(spec)
|
|
19
|
+
: { addresses: [], label: "" };
|
|
52
20
|
---
|
|
53
21
|
|
|
54
22
|
{
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import specs from "blume:openapi";
|
|
3
|
+
import { specOf } from "../../openapi/model.ts";
|
|
3
4
|
import { withBase } from "../islands/base-path.ts";
|
|
4
5
|
import MethodBadge from "./MethodBadge.astro";
|
|
5
6
|
|
|
@@ -14,7 +15,7 @@ interface Props {
|
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
const { source, tag } = Astro.props;
|
|
17
|
-
const operations = Object.values(specs
|
|
18
|
+
const operations = Object.values(specOf(specs, source)?.operations ?? {}).filter(
|
|
18
19
|
(operation) => operation.tagSlug === tag
|
|
19
20
|
);
|
|
20
21
|
---
|
|
@@ -4,6 +4,7 @@ import specs from "blume:openapi";
|
|
|
4
4
|
|
|
5
5
|
import type { AsyncApiDocument } from "../../openapi/asyncapi.ts";
|
|
6
6
|
import { asyncApiOperationObject } from "../../openapi/asyncapi.ts";
|
|
7
|
+
import { operationOf, specOf } from "../../openapi/model.ts";
|
|
7
8
|
import { highlightCode } from "../../markdown/index.ts";
|
|
8
9
|
import {
|
|
9
10
|
asyncApiSecurityEntries,
|
|
@@ -22,6 +23,7 @@ import { asyncSampleLanguages } from "./async-snippets.ts";
|
|
|
22
23
|
import { languageSamplePanels } from "./sample-panels.ts";
|
|
23
24
|
import { buildMessage, defaultMessageValues } from "./message.ts";
|
|
24
25
|
import { messageModel } from "./message-model.ts";
|
|
26
|
+
import Description from "./Description.astro";
|
|
25
27
|
import MessageComposer from "./MessageComposer.astro";
|
|
26
28
|
import Authorization from "./Authorization.astro";
|
|
27
29
|
import Bindings from "./Bindings.astro";
|
|
@@ -47,8 +49,8 @@ interface Props {
|
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
const { source, id } = Astro.props;
|
|
50
|
-
const spec = specs
|
|
51
|
-
const ref = spec
|
|
52
|
+
const spec = specOf(specs, source);
|
|
53
|
+
const ref = spec ? operationOf(spec, id) : undefined;
|
|
52
54
|
const document = (spec?.document ?? {}) as AsyncApiDocument;
|
|
53
55
|
const operation = ref ? asyncApiOperationObject(document, ref) : undefined;
|
|
54
56
|
const channel = ref?.channelId
|
|
@@ -155,9 +157,10 @@ const channelBindings = bindingGroups(channel?.bindings);
|
|
|
155
157
|
: "Message"}
|
|
156
158
|
</div>
|
|
157
159
|
{named.message.description && (
|
|
158
|
-
<
|
|
159
|
-
|
|
160
|
-
|
|
160
|
+
<Description
|
|
161
|
+
class="mb-2 text-muted-foreground text-sm"
|
|
162
|
+
text={named.message.description}
|
|
163
|
+
/>
|
|
161
164
|
)}
|
|
162
165
|
{named.message.contentType && (
|
|
163
166
|
<div class="mb-2 text-muted-foreground text-xs">
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
import Description from "./Description.astro";
|
|
2
3
|
import {
|
|
3
4
|
type OperationSecurity,
|
|
4
5
|
schemeCarrier,
|
|
@@ -38,7 +39,7 @@ const { security } = Astro.props;
|
|
|
38
39
|
{alternative.map((resolved) => {
|
|
39
40
|
const carrier = schemeCarrier(resolved);
|
|
40
41
|
return (
|
|
41
|
-
<div class="border-border border-t py-
|
|
42
|
+
<div class="border-border border-t py-4 first:border-t-0">
|
|
42
43
|
<div class="flex flex-wrap items-baseline gap-x-2 gap-y-1">
|
|
43
44
|
<code class="font-mono text-foreground text-sm">
|
|
44
45
|
{carrier?.name ?? resolved.key}
|
|
@@ -54,13 +55,10 @@ const { security } = Astro.props;
|
|
|
54
55
|
)}
|
|
55
56
|
</div>
|
|
56
57
|
{resolved.scheme?.description && (
|
|
57
|
-
<
|
|
58
|
-
class="mt-1 text-muted-foreground text-sm"
|
|
59
|
-
set:text={resolved.scheme.description}
|
|
60
|
-
/>
|
|
58
|
+
<Description class="mt-2 text-muted-foreground text-sm" text={resolved.scheme.description} />
|
|
61
59
|
)}
|
|
62
60
|
{resolved.scopes.length > 0 && (
|
|
63
|
-
<div class="mt-
|
|
61
|
+
<div class="mt-2 flex flex-wrap items-center gap-1.5 text-xs">
|
|
64
62
|
<span class="text-muted-foreground">Scopes:</span>
|
|
65
63
|
{resolved.scopes.map((scope) => (
|
|
66
64
|
<code class="rounded bg-muted px-1 py-0.5 text-foreground">
|
|
@@ -54,13 +54,13 @@ const isSchemaish = (value: unknown): value is SchemaLike => {
|
|
|
54
54
|
</div>
|
|
55
55
|
{groups.map((group) => (
|
|
56
56
|
<div class="not-prose mb-3 rounded-blume border border-border px-4 last:mb-0">
|
|
57
|
-
<div class="flex items-baseline gap-2 border-border py-
|
|
57
|
+
<div class="flex items-baseline gap-2 border-border py-4">
|
|
58
58
|
<code class="font-mono font-semibold text-foreground text-sm">
|
|
59
59
|
{group.protocol}
|
|
60
60
|
</code>
|
|
61
61
|
</div>
|
|
62
62
|
{group.rows.map((row) => (
|
|
63
|
-
<div class="border-border border-t py-
|
|
63
|
+
<div class="border-border border-t py-4">
|
|
64
64
|
<div class="flex flex-wrap items-baseline gap-x-2 gap-y-1">
|
|
65
65
|
<code class="font-mono text-foreground text-sm">{row.name}</code>
|
|
66
66
|
{!isSchemaish(row.value) && (
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { descriptionHtml } from "./description.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A spec description, rendered as Markdown.
|
|
6
|
+
*
|
|
7
|
+
* One component rather than the same three lines in six places, and that is not only tidiness:
|
|
8
|
+
* the styling below is emitted ONCE per page here, where writing it as utility classes on each
|
|
9
|
+
* element repeated it per description instead. On the largest reference page that is 2,400
|
|
10
|
+
* descriptions — the first attempt at this put 1.5 MB of identical class attributes into a single
|
|
11
|
+
* HTML document and pushed eight pages past Googlebot's 2 MB crawl limit, which `blume audit`
|
|
12
|
+
* caught. A class name and one stylesheet cost the same at one description as at two thousand.
|
|
13
|
+
*/
|
|
14
|
+
interface Props {
|
|
15
|
+
/** Extra classes for the wrapper — position and base type come from the caller. */
|
|
16
|
+
class?: string;
|
|
17
|
+
text?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { class: className = "", text } = Astro.props;
|
|
21
|
+
const html = descriptionHtml(text);
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
{html && <div class:list={["blume-api-description", className]} set:html={html} />}
|
|
25
|
+
|
|
26
|
+
<style is:global>
|
|
27
|
+
/* Plain CSS on a single class, not the typography plugin's `prose`. `prose` sets its own font
|
|
28
|
+
size, colour and rhythm, all of which fight the muted small type these sit in — a property
|
|
29
|
+
description would come out larger and darker than the property name above it. Only what
|
|
30
|
+
Markdown needs is styled; everything else inherits, so a description still reads as
|
|
31
|
+
annotation rather than as body copy. */
|
|
32
|
+
.blume-api-description > :first-child {
|
|
33
|
+
margin-top: 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.blume-api-description > :last-child {
|
|
37
|
+
margin-bottom: 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.blume-api-description p,
|
|
41
|
+
.blume-api-description ol,
|
|
42
|
+
.blume-api-description ul,
|
|
43
|
+
.blume-api-description pre {
|
|
44
|
+
margin-block: 0.5rem;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.blume-api-description ol {
|
|
48
|
+
list-style: decimal;
|
|
49
|
+
padding-inline-start: 1.25rem;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.blume-api-description ul {
|
|
53
|
+
list-style: disc;
|
|
54
|
+
padding-inline-start: 1.25rem;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.blume-api-description li {
|
|
58
|
+
margin-block: 0.25rem;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* A heading in a description is a label for the paragraph under it, not a section of the
|
|
62
|
+
page: it keeps the description's own size and only gains the weight and colour of a label.
|
|
63
|
+
Without this it inherits the reset's unstyled heading and is indistinguishable from text. */
|
|
64
|
+
.blume-api-description :is(h1, h2, h3, h4, h5, h6) {
|
|
65
|
+
color: var(--color-foreground);
|
|
66
|
+
font-weight: 600;
|
|
67
|
+
margin-block: 0.5rem;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.blume-api-description strong {
|
|
71
|
+
color: var(--color-foreground);
|
|
72
|
+
font-weight: 600;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.blume-api-description a {
|
|
76
|
+
text-decoration: underline;
|
|
77
|
+
text-underline-offset: 2px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* Matched to the chip the reference already draws for an enum value, so an `apiKey` in prose
|
|
81
|
+
looks like the `apiKey` in the chips beside it. */
|
|
82
|
+
.blume-api-description code {
|
|
83
|
+
background: var(--color-muted);
|
|
84
|
+
border-radius: 0.25rem;
|
|
85
|
+
color: var(--color-foreground);
|
|
86
|
+
font-family: var(--font-mono);
|
|
87
|
+
font-size: 0.75rem;
|
|
88
|
+
padding: 0.125rem 0.25rem;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.blume-api-description pre {
|
|
92
|
+
background: var(--color-muted);
|
|
93
|
+
border-radius: 0.25rem;
|
|
94
|
+
overflow-x: auto;
|
|
95
|
+
padding: 0.5rem;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.blume-api-description pre code {
|
|
99
|
+
background: none;
|
|
100
|
+
padding: 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* The scroll frame a table gets from `descriptionHtml` carries the body's own 1.5rem rhythm,
|
|
104
|
+
which is three times what everything else in a description sits at. Only the margin is
|
|
105
|
+
restated; the frame, the cell padding and the scrolling are the renderer's. */
|
|
106
|
+
.blume-api-description .blume-table-scroll {
|
|
107
|
+
margin-block: 0.5rem;
|
|
108
|
+
}
|
|
109
|
+
</style>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
import type { GraphqlFieldRow } from "./graphql-helpers.ts";
|
|
3
3
|
import { isOutputField } from "./graphql-helpers.ts";
|
|
4
|
+
import Description from "./Description.astro";
|
|
4
5
|
import GraphqlChip from "./GraphqlChip.astro";
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -46,7 +47,7 @@ const isRequired = (row: GraphqlFieldRow): boolean =>
|
|
|
46
47
|
const route = routes.get(row.type.name);
|
|
47
48
|
const args = isOutputField(row) ? row.args : [];
|
|
48
49
|
return (
|
|
49
|
-
<div class="border-border border-t py-
|
|
50
|
+
<div class="border-border border-t py-4 first:border-t-0">
|
|
50
51
|
<div class="flex flex-wrap items-baseline gap-x-2 gap-y-1">
|
|
51
52
|
<code class="font-mono text-foreground text-sm">
|
|
52
53
|
{row.name}
|
|
@@ -68,13 +69,10 @@ const isRequired = (row: GraphqlFieldRow): boolean =>
|
|
|
68
69
|
)}
|
|
69
70
|
</div>
|
|
70
71
|
{row.description && (
|
|
71
|
-
<
|
|
72
|
-
class="mt-1 text-muted-foreground text-sm"
|
|
73
|
-
set:text={row.description}
|
|
74
|
-
/>
|
|
72
|
+
<Description class="mt-2 text-muted-foreground text-sm" text={row.description} />
|
|
75
73
|
)}
|
|
76
74
|
{"default" in row && row.default !== undefined && (
|
|
77
|
-
<div class="mt-
|
|
75
|
+
<div class="mt-2 text-muted-foreground text-xs">
|
|
78
76
|
Default:{" "}
|
|
79
77
|
<code class="rounded bg-muted px-1 py-0.5 text-foreground">
|
|
80
78
|
{row.default}
|
|
@@ -82,7 +80,7 @@ const isRequired = (row: GraphqlFieldRow): boolean =>
|
|
|
82
80
|
</div>
|
|
83
81
|
)}
|
|
84
82
|
{row.deprecationReason && (
|
|
85
|
-
<div class="mt-
|
|
83
|
+
<div class="mt-2 text-muted-foreground text-xs">
|
|
86
84
|
Deprecated: <span set:text={row.deprecationReason} />
|
|
87
85
|
</div>
|
|
88
86
|
)}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
graphqlRootField,
|
|
11
11
|
isGraphqlOperationKind,
|
|
12
12
|
} from "../../openapi/graphql.ts";
|
|
13
|
+
import { operationOf, specOf } from "../../openapi/model.ts";
|
|
13
14
|
import { highlightCode } from "../../markdown/index.ts";
|
|
14
15
|
import { DEPRECATED_LABEL_CLASS } from "../colors.ts";
|
|
15
16
|
import {
|
|
@@ -22,6 +23,7 @@ import {
|
|
|
22
23
|
import { buildRequest, defaultValues } from "./request.ts";
|
|
23
24
|
import { languageSamplePanels } from "./sample-panels.ts";
|
|
24
25
|
import { sampleLanguages } from "./snippets.ts";
|
|
26
|
+
import Description from "./Description.astro";
|
|
25
27
|
import GraphqlChip from "./GraphqlChip.astro";
|
|
26
28
|
import GraphqlFieldsTable from "./GraphqlFieldsTable.astro";
|
|
27
29
|
import GraphqlType from "./GraphqlType.astro";
|
|
@@ -46,8 +48,8 @@ interface Props {
|
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
const { source, id } = Astro.props;
|
|
49
|
-
const spec = specs
|
|
50
|
-
const ref = spec
|
|
51
|
+
const spec = specOf(specs, source);
|
|
52
|
+
const ref = spec ? operationOf(spec, id) : undefined;
|
|
51
53
|
const document = (spec?.document ?? { roots: {}, types: {} }) as GraphqlDocument;
|
|
52
54
|
const isOperation = ref !== undefined && isGraphqlOperationKind(ref.method);
|
|
53
55
|
const field = ref && isOperation ? graphqlRootField(document, ref) : undefined;
|
|
@@ -153,9 +155,9 @@ const returnRoute = field ? routes.get(field.type.name) : undefined;
|
|
|
153
155
|
/>
|
|
154
156
|
</div>
|
|
155
157
|
{document.types[field.type.name]?.description && (
|
|
156
|
-
<
|
|
157
|
-
class="mt-
|
|
158
|
-
|
|
158
|
+
<Description
|
|
159
|
+
class="mt-2 text-muted-foreground text-sm"
|
|
160
|
+
text={document.types[field.type.name]?.description}
|
|
159
161
|
/>
|
|
160
162
|
)}
|
|
161
163
|
</section>
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
graphqlRoutes,
|
|
8
8
|
graphqlUsage,
|
|
9
9
|
} from "./graphql-helpers.ts";
|
|
10
|
+
import Description from "./Description.astro";
|
|
10
11
|
import GraphqlChip from "./GraphqlChip.astro";
|
|
11
12
|
import GraphqlFieldsTable from "./GraphqlFieldsTable.astro";
|
|
12
13
|
import MethodBadge from "./MethodBadge.astro";
|
|
@@ -86,7 +87,7 @@ const SECTION_HEADING = "mb-2 font-semibold text-foreground text-sm";
|
|
|
86
87
|
</div>
|
|
87
88
|
<div class="rounded-blume border border-border px-4">
|
|
88
89
|
{(type.enumValues ?? []).map((value) => (
|
|
89
|
-
<div class="border-border border-t py-
|
|
90
|
+
<div class="border-border border-t py-4 first:border-t-0">
|
|
90
91
|
<div class="flex flex-wrap items-baseline gap-x-2 gap-y-1">
|
|
91
92
|
<code class="font-mono text-foreground text-sm">
|
|
92
93
|
{value.name}
|
|
@@ -98,13 +99,10 @@ const SECTION_HEADING = "mb-2 font-semibold text-foreground text-sm";
|
|
|
98
99
|
)}
|
|
99
100
|
</div>
|
|
100
101
|
{value.description && (
|
|
101
|
-
<
|
|
102
|
-
class="mt-1 text-muted-foreground text-sm"
|
|
103
|
-
set:text={value.description}
|
|
104
|
-
/>
|
|
102
|
+
<Description class="mt-2 text-muted-foreground text-sm" text={value.description} />
|
|
105
103
|
)}
|
|
106
104
|
{value.deprecationReason && (
|
|
107
|
-
<div class="mt-
|
|
105
|
+
<div class="mt-2 text-muted-foreground text-xs">
|
|
108
106
|
Deprecated: <span set:text={value.deprecationReason} />
|
|
109
107
|
</div>
|
|
110
108
|
)}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import specs from "blume:openapi";
|
|
3
|
+
import { operationOf, specOf } from "../../openapi/model.ts";
|
|
3
4
|
import {
|
|
4
5
|
mergeParameters,
|
|
5
6
|
type ParameterLike,
|
|
@@ -61,8 +62,8 @@ interface FullOperation {
|
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
const { source, id } = Astro.props;
|
|
64
|
-
const spec = specs
|
|
65
|
-
const ref = spec
|
|
65
|
+
const spec = specOf(specs, source);
|
|
66
|
+
const ref = spec ? operationOf(spec, id) : undefined;
|
|
66
67
|
// The AsyncAPI and GraphQL front-ends render their own bodies; the lookups
|
|
67
68
|
// below are OpenAPI-shaped (paths, request/response) and resolve to nothing
|
|
68
69
|
// for them.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
import Description from "./Description.astro";
|
|
2
3
|
import {
|
|
3
4
|
constraints,
|
|
4
5
|
resolveSchema,
|
|
@@ -54,7 +55,7 @@ const groups = SECTIONS.map((section) => ({
|
|
|
54
55
|
const limits = constraints(resolved);
|
|
55
56
|
const enumValues = Array.isArray(resolved.enum) ? resolved.enum : null;
|
|
56
57
|
return (
|
|
57
|
-
<div class="border-border border-t py-
|
|
58
|
+
<div class="border-border border-t py-4 first:border-t-0">
|
|
58
59
|
<div class="flex flex-wrap items-baseline gap-x-2 gap-y-1">
|
|
59
60
|
<code class="font-mono text-foreground text-sm">{param.name}</code>
|
|
60
61
|
<span class="text-muted-foreground text-xs">{type}</span>
|
|
@@ -70,18 +71,15 @@ const groups = SECTIONS.map((section) => ({
|
|
|
70
71
|
)}
|
|
71
72
|
</div>
|
|
72
73
|
{param.description && (
|
|
73
|
-
<
|
|
74
|
-
class="mt-1 text-muted-foreground text-sm"
|
|
75
|
-
set:text={param.description}
|
|
76
|
-
/>
|
|
74
|
+
<Description class="mt-2 text-muted-foreground text-sm" text={param.description} />
|
|
77
75
|
)}
|
|
78
76
|
{limits.length > 0 && (
|
|
79
|
-
<div class="mt-
|
|
77
|
+
<div class="mt-2 text-muted-foreground text-xs">
|
|
80
78
|
{limits.join(" · ")}
|
|
81
79
|
</div>
|
|
82
80
|
)}
|
|
83
81
|
{enumValues && (
|
|
84
|
-
<div class="mt-
|
|
82
|
+
<div class="mt-2 flex flex-wrap items-center gap-1.5 text-xs">
|
|
85
83
|
<span class="text-muted-foreground">Allowed:</span>
|
|
86
84
|
{enumValues.map((value) => (
|
|
87
85
|
<code class="rounded bg-muted px-1 py-0.5 text-foreground">
|