@stainless-api/docs 0.1.0-beta.99 → 1.0.0-beta.140
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 +390 -0
- package/ambient.d.ts +6 -0
- package/eslint-suppressions.json +22 -6
- package/{eslint.config.js → eslint.config.ts} +1 -7
- package/package.json +57 -40
- package/plugin/buildAlgoliaIndex.ts +6 -12
- package/plugin/components/SDKSelect.astro +0 -6
- package/plugin/components/SnippetCode.tsx +6 -37
- package/plugin/components/search/SearchAlgolia.astro +1 -1
- package/plugin/components/search/SearchIsland.tsx +19 -13
- package/plugin/generateAPIReferenceLink.ts +0 -40
- package/plugin/globalJs/ai-dropdown-options.ts +22 -9
- package/plugin/globalJs/code-snippets.ts +5 -5
- package/plugin/globalJs/copy.ts +20 -91
- package/plugin/globalJs/navigation.ts +13 -13
- package/plugin/globalJs/summary-selection-tweak.ts +29 -0
- package/plugin/index.ts +107 -163
- package/plugin/loadPluginConfig.ts +49 -151
- package/plugin/markdown/highlighter.ts +100 -0
- package/plugin/markdown/index.ts +39 -0
- package/plugin/middlewareBuilder/stainlessMiddleware.d.ts +2 -0
- package/plugin/react/Routing.tsx +10 -244
- package/plugin/referencePlaceholderUtils.ts +1 -1
- package/plugin/replaceSidebarPlaceholderMiddleware.ts +1 -1
- package/plugin/routes/Docs.astro +3 -1
- package/plugin/routes/Overview.astro +14 -7
- package/plugin/routes/llms.ts +186 -0
- package/plugin/routes/markdown.ts +62 -13
- package/plugin/sidebar-utils/sidebar-builder.ts +38 -12
- package/plugin/specs/defaultSpecLoader.ts +192 -0
- package/plugin/specs/fetchSpecSSR.ts +1 -1
- package/plugin/specs/utils.ts +86 -0
- package/shared/conditionalIntegration.ts +28 -0
- package/shared/getProsePages.ts +6 -7
- package/shared/virtualModule.ts +1 -26
- package/stl-docs/aiChatExamples.ts +31 -0
- package/stl-docs/chat/docs-chat-handler.ts +17 -0
- package/stl-docs/chat/hook.ts +225 -0
- package/stl-docs/chat/schemas.ts +27 -0
- package/stl-docs/chat/ui/AiChat.module.css +591 -0
- package/stl-docs/chat/ui/AiChat.tsx +175 -0
- package/stl-docs/chat/ui/Trigger.tsx +154 -0
- package/stl-docs/chat/ui/components/ChatControls.tsx +51 -0
- package/stl-docs/chat/ui/components/ChatEmpty.tsx +42 -0
- package/stl-docs/chat/ui/components/ChatLog.tsx +93 -0
- package/stl-docs/chat/ui/components/ChatMessage.tsx +47 -0
- package/stl-docs/chat/ui/components/CodeBlock.tsx +33 -0
- package/stl-docs/chat/ui/components/MessageFeedback.tsx +106 -0
- package/stl-docs/chat/ui/components/Table.tsx +15 -0
- package/stl-docs/chat/ui/components/ToolCall.tsx +34 -0
- package/stl-docs/chat/ui/components/hljs-github.css +81 -0
- package/stl-docs/chat/ui/scroll-manager.ts +86 -0
- package/stl-docs/chat/ui/types.ts +45 -0
- package/stl-docs/components/AiChatIsland.tsx +10 -12
- package/stl-docs/components/ContentPanel.astro +9 -0
- package/stl-docs/components/Footer.astro +89 -0
- package/stl-docs/components/Header.astro +0 -5
- package/stl-docs/components/PageFrame.astro +23 -8
- package/stl-docs/components/PageSidebar.astro +11 -0
- package/stl-docs/components/StainlessLogo.svg +4 -0
- package/stl-docs/components/TwoColumnContent.astro +2 -0
- package/stl-docs/components/headers/DefaultHeader.astro +6 -8
- package/stl-docs/components/headers/StackedHeader.astro +5 -53
- package/stl-docs/components/mintlify-compat/Accordion.astro +2 -2
- package/stl-docs/components/mintlify-compat/AccordionGroup.astro +0 -4
- package/stl-docs/components/mintlify-compat/Columns.astro +2 -2
- package/stl-docs/components/mintlify-compat/Frame.astro +2 -2
- package/stl-docs/components/mintlify-compat/Tab.astro +2 -2
- package/stl-docs/components/mintlify-compat/callouts/Callout.astro +2 -2
- package/stl-docs/components/mintlify-compat/callouts/Check.astro +0 -4
- package/stl-docs/components/mintlify-compat/callouts/Danger.astro +0 -4
- package/stl-docs/components/mintlify-compat/callouts/Info.astro +0 -4
- package/stl-docs/components/mintlify-compat/callouts/Note.astro +0 -4
- package/stl-docs/components/mintlify-compat/callouts/Tip.astro +0 -4
- package/stl-docs/components/mintlify-compat/callouts/Warning.astro +0 -4
- package/stl-docs/components/nav-tabs/NavDropdown.astro +12 -7
- package/stl-docs/components/nav-tabs/NavTabs.astro +5 -3
- package/stl-docs/components/nav-tabs/buildNavLinks.ts +2 -0
- package/stl-docs/components/pagination/Pagination.astro +4 -2
- package/stl-docs/components/pagination/PaginationLinkEmphasized.astro +2 -2
- package/stl-docs/components/pagination/PaginationLinkQuiet.astro +2 -2
- package/stl-docs/components/pagination/util.ts +3 -3
- package/stl-docs/components/sidebars/BaseSidebar.astro +72 -1
- package/stl-docs/disableCalloutSyntax.ts +1 -1
- package/stl-docs/fonts.ts +5 -5
- package/stl-docs/index.ts +76 -53
- package/stl-docs/loadStlDocsConfig.ts +38 -8
- package/stl-docs/og-image/components/OpenGraphFunctionSignature.tsx +64 -0
- package/stl-docs/og-image/components/OpenGraphImage.tsx +126 -0
- package/stl-docs/og-image/config.ts +56 -0
- package/stl-docs/og-image/image-gen/generate-api-reference-og-image.tsx +188 -0
- package/stl-docs/og-image/image-gen/generate-og-image.tsx +119 -0
- package/stl-docs/og-image/image-gen/get-logo-url.ts +47 -0
- package/stl-docs/og-image/index.ts +135 -0
- package/stl-docs/og-image/routes/add-og-image.ts +45 -0
- package/stl-docs/og-image/routes/get-api-reference-og-image.ts +36 -0
- package/stl-docs/og-image/routes/get-og-image.ts +28 -0
- package/stl-docs/og-image/theme.ts +43 -0
- package/stl-docs/og-image/utils.ts +14 -0
- package/stl-docs/proseDocSync.test.ts +74 -0
- package/stl-docs/proseDocSync.ts +344 -0
- package/stl-docs/proseMarkdown/proseMarkdownIntegration.ts +4 -12
- package/stl-docs/schema-extension.ts +12 -0
- package/stl-docs/tabsMiddleware.ts +1 -1
- package/styles/overrides.css +2 -14
- package/styles/page.css +210 -71
- package/styles/sidebar.css +30 -17
- package/styles/sl-variables.css +3 -8
- package/styles/stldocs-variables.css +2 -2
- package/styles/toc.css +8 -0
- package/tsconfig.json +1 -1
- package/virtual-module.d.ts +35 -11
- package/playground-virtual-modules.d.ts +0 -96
- package/plugin/globalJs/create-playground.shim.ts +0 -3
- package/plugin/globalJs/playground-data.shim.ts +0 -1
- package/plugin/globalJs/playground-data.ts +0 -14
- package/plugin/specs/FileCache.ts +0 -99
- package/plugin/specs/generateSpec.ts +0 -112
- package/plugin/specs/index.ts +0 -132
- package/plugin/specs/inputResolver.ts +0 -146
- package/plugin/specs/worker.ts +0 -199
- package/plugin/vendor/preview.worker.docs.js +0 -26108
- package/plugin/vendor/templates/cli.md +0 -1
- package/plugin/vendor/templates/go.md +0 -316
- package/plugin/vendor/templates/java.md +0 -89
- package/plugin/vendor/templates/kotlin.md +0 -89
- package/plugin/vendor/templates/node.md +0 -235
- package/plugin/vendor/templates/python.md +0 -251
- package/plugin/vendor/templates/ruby.md +0 -147
- package/plugin/vendor/templates/terraform.md +0 -60
- package/plugin/vendor/templates/typescript.md +0 -319
- package/scripts/vendor_deps.ts +0 -50
- package/stl-docs/components/ClientRouterHead.astro +0 -41
- package/stl-docs/components/content-panel/ContentPanel.astro +0 -42
- package/stl-docs/components/headers/SplashMobileMenuToggle.astro +0 -65
- package/stl-docs/proseSearchIndexing.ts +0 -606
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { ImageResponse } from 'takumi-js/response';
|
|
2
|
+
import { generateDocsRoutes } from '@stainless-api/docs/generate-docs-routes';
|
|
3
|
+
import { DocsLanguage, parseStainlessPath } from '@stainless-api/docs-ui/routing';
|
|
4
|
+
import { getResourceFromSpec } from '@stainless-api/docs-ui/utils';
|
|
5
|
+
import { ArrowDownLeft, ArrowUpRight, XIcon } from 'lucide-react';
|
|
6
|
+
import OpenGraphImage from 'virtual:stainless-docs/docs-og-image/components/OpenGraphImage';
|
|
7
|
+
import OpenGraphFunctionSignature from 'virtual:stainless-docs/docs-og-image/components/OpenGraphFunctionSignature';
|
|
8
|
+
import { LanguageDeclNodes, Method } from '@stainless/sdk-json';
|
|
9
|
+
import getLogoDataUrl from './get-logo-url';
|
|
10
|
+
import { notFoundResponse, renderOptions } from '../utils';
|
|
11
|
+
import { OG_IMAGE_OPTIONS } from 'virtual:stainless-docs/docs-og-image';
|
|
12
|
+
import { darkThemeVars, lightThemeVars } from '../theme';
|
|
13
|
+
import { generateApiBreadcrumbs } from '@stainless-api/docs-ui/components';
|
|
14
|
+
import { getSDKJSONInSSR } from '@stainless-api/docs/specs/fetchSpecSSR';
|
|
15
|
+
import { RESOLVED_API_REFERENCE_PATH } from 'virtual:stl-starlight-virtual-module';
|
|
16
|
+
import type * as SDKJSON from '@stainless/sdk-json';
|
|
17
|
+
|
|
18
|
+
type ApiReferenceRoute = ReturnType<typeof generateDocsRoutes>[number];
|
|
19
|
+
|
|
20
|
+
export default async function generateApiReferenceOgImage({
|
|
21
|
+
apiReferenceRoute,
|
|
22
|
+
slug,
|
|
23
|
+
}: {
|
|
24
|
+
apiReferenceRoute?: ApiReferenceRoute;
|
|
25
|
+
slug: string;
|
|
26
|
+
}) {
|
|
27
|
+
if (!apiReferenceRoute?.props.stainlessPath) return notFoundResponse();
|
|
28
|
+
|
|
29
|
+
const spec = await getSDKJSONInSSR(apiReferenceRoute.props.language);
|
|
30
|
+
|
|
31
|
+
const parsed = parseStainlessPath(apiReferenceRoute.props.stainlessPath);
|
|
32
|
+
const resource = getResourceFromSpec(apiReferenceRoute.props.stainlessPath, spec);
|
|
33
|
+
|
|
34
|
+
if (!resource || !parsed?.method || !resource.methods[parsed.method]) return notFoundResponse();
|
|
35
|
+
|
|
36
|
+
if (apiReferenceRoute.props.kind === 'http_method') {
|
|
37
|
+
const method = resource.methods[parsed.method]!;
|
|
38
|
+
return generateApiReferenceMethodOgImage({
|
|
39
|
+
method,
|
|
40
|
+
language: apiReferenceRoute.props.language,
|
|
41
|
+
stainlessPath: apiReferenceRoute.props.stainlessPath,
|
|
42
|
+
slug: `${RESOLVED_API_REFERENCE_PATH}/${slug}`,
|
|
43
|
+
spec,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const logoDataUrl = getLogoDataUrl();
|
|
48
|
+
|
|
49
|
+
return new ImageResponse(
|
|
50
|
+
<OpenGraphImage
|
|
51
|
+
title={resource.title}
|
|
52
|
+
description={`API Overview - ${apiReferenceRoute.props.language} `}
|
|
53
|
+
logo={logoDataUrl}
|
|
54
|
+
/>,
|
|
55
|
+
renderOptions,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function generateApiReferenceMethodOgImage({
|
|
60
|
+
method,
|
|
61
|
+
language,
|
|
62
|
+
stainlessPath,
|
|
63
|
+
slug,
|
|
64
|
+
spec,
|
|
65
|
+
}: {
|
|
66
|
+
method: Method;
|
|
67
|
+
language: DocsLanguage;
|
|
68
|
+
stainlessPath: string;
|
|
69
|
+
slug: string;
|
|
70
|
+
spec: SDKJSON.Spec;
|
|
71
|
+
}) {
|
|
72
|
+
const slugWithoutExtension = slug.replace(/\.[^/.]+$/, '');
|
|
73
|
+
const endpoint = method.endpoint.slice(method.endpoint.indexOf(' ') + 1);
|
|
74
|
+
const httpMethod = method.httpMethod.toUpperCase();
|
|
75
|
+
|
|
76
|
+
const decl = spec?.decls?.[language]?.[stainlessPath] as LanguageDeclNodes[keyof LanguageDeclNodes];
|
|
77
|
+
|
|
78
|
+
if (!decl) {
|
|
79
|
+
return notFoundResponse();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let params: { ident: string; optional?: boolean }[] | undefined = undefined;
|
|
83
|
+
let qualified: string | undefined = undefined;
|
|
84
|
+
|
|
85
|
+
if ('signature' in decl && decl.signature) {
|
|
86
|
+
params = decl.signature.parameters;
|
|
87
|
+
} else if ('parameters' in decl && decl.parameters) {
|
|
88
|
+
// @ts-expect-error TODO: this is breaking builds
|
|
89
|
+
params = decl.parameters;
|
|
90
|
+
} else if ('args' in decl && decl.args) {
|
|
91
|
+
params = decl.args;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if ('qualified' in decl && decl.qualified) {
|
|
95
|
+
qualified = decl.qualified;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const logoDataUrl = getLogoDataUrl();
|
|
99
|
+
const colors = OG_IMAGE_OPTIONS?.theme === 'dark' ? darkThemeVars : lightThemeVars;
|
|
100
|
+
const httpColors =
|
|
101
|
+
httpMethod === 'GET'
|
|
102
|
+
? { background: colors.greenBackground, text: colors.green }
|
|
103
|
+
: httpMethod === 'POST'
|
|
104
|
+
? { background: colors.blueBackground, text: colors.blue }
|
|
105
|
+
: httpMethod === 'PUT' || httpMethod === 'PATCH'
|
|
106
|
+
? { background: colors.orangeBackground, text: colors.orange }
|
|
107
|
+
: httpMethod === 'DELETE'
|
|
108
|
+
? { background: colors.redBackground, text: colors.red }
|
|
109
|
+
: { background: colors.foregroundMuted, text: colors.foreground };
|
|
110
|
+
// remove first and last breadcrumb (API Reference and current page)
|
|
111
|
+
const breadcrumbs = generateApiBreadcrumbs(
|
|
112
|
+
slugWithoutExtension,
|
|
113
|
+
spec,
|
|
114
|
+
RESOLVED_API_REFERENCE_PATH || '/api',
|
|
115
|
+
)?.slice(1, -1);
|
|
116
|
+
|
|
117
|
+
return new ImageResponse(
|
|
118
|
+
<OpenGraphImage
|
|
119
|
+
title={method.summary || method.title}
|
|
120
|
+
logo={logoDataUrl}
|
|
121
|
+
theme={OG_IMAGE_OPTIONS?.theme}
|
|
122
|
+
breadcrumbs={breadcrumbs ? breadcrumbs.map((b) => b.title) : undefined}
|
|
123
|
+
>
|
|
124
|
+
<div
|
|
125
|
+
style={{
|
|
126
|
+
display: 'flex',
|
|
127
|
+
flexDirection: 'column',
|
|
128
|
+
fontFamily: 'monospace',
|
|
129
|
+
}}
|
|
130
|
+
>
|
|
131
|
+
<OpenGraphFunctionSignature
|
|
132
|
+
params={params}
|
|
133
|
+
fullyQualifiedName={qualified}
|
|
134
|
+
theme={OG_IMAGE_OPTIONS?.theme}
|
|
135
|
+
/>
|
|
136
|
+
<div
|
|
137
|
+
style={{
|
|
138
|
+
display: 'flex',
|
|
139
|
+
gap: '8px',
|
|
140
|
+
alignItems: 'center',
|
|
141
|
+
fontFamily: 'monospace',
|
|
142
|
+
}}
|
|
143
|
+
>
|
|
144
|
+
<div
|
|
145
|
+
style={{
|
|
146
|
+
display: 'flex',
|
|
147
|
+
alignItems: 'center',
|
|
148
|
+
justifyContent: 'center',
|
|
149
|
+
paddingLeft: '2px',
|
|
150
|
+
paddingRight: '6px',
|
|
151
|
+
paddingTop: '4px',
|
|
152
|
+
paddingBottom: '4px',
|
|
153
|
+
borderRadius: '8px',
|
|
154
|
+
fontWeight: 600,
|
|
155
|
+
color: httpColors.text,
|
|
156
|
+
backgroundColor: httpColors.background,
|
|
157
|
+
lineHeight: '100%',
|
|
158
|
+
fontSize: '25px',
|
|
159
|
+
stroke: colors.foreground,
|
|
160
|
+
fontFamily: 'monospace',
|
|
161
|
+
flexShrink: 0,
|
|
162
|
+
}}
|
|
163
|
+
>
|
|
164
|
+
{httpMethod === 'GET' && <ArrowDownLeft size={36} color={colors.green} />}
|
|
165
|
+
{httpMethod === 'POST' && <ArrowUpRight size={36} color={colors.blue} />}
|
|
166
|
+
{(httpMethod === 'PUT' || httpMethod === 'PATCH') && (
|
|
167
|
+
<ArrowUpRight size={36} color={colors.orange} />
|
|
168
|
+
)}
|
|
169
|
+
{httpMethod === 'DELETE' && <XIcon size={36} color={colors.red} />}
|
|
170
|
+
{httpMethod}
|
|
171
|
+
</div>
|
|
172
|
+
<div
|
|
173
|
+
style={{
|
|
174
|
+
lineClamp: 1,
|
|
175
|
+
overflow: 'hidden',
|
|
176
|
+
textOverflow: 'ellipsis',
|
|
177
|
+
color: colors.foregroundMuted,
|
|
178
|
+
fontFamily: 'monospace',
|
|
179
|
+
}}
|
|
180
|
+
>
|
|
181
|
+
{endpoint}
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
</OpenGraphImage>,
|
|
186
|
+
renderOptions,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { getCollection } from 'astro:content';
|
|
2
|
+
import { z } from 'astro/zod';
|
|
3
|
+
import { ImageResponse } from 'takumi-js/response';
|
|
4
|
+
import { Tabs } from '@stainless-api/docs/docs-config';
|
|
5
|
+
import OpenGraphImage from 'virtual:stainless-docs/docs-og-image/components/OpenGraphImage';
|
|
6
|
+
import { OG_IMAGE_OPTIONS } from 'virtual:stainless-docs/docs-og-image';
|
|
7
|
+
import { TABS } from 'virtual:stl-docs-virtual-module';
|
|
8
|
+
import getLogoDataUrl from './get-logo-url';
|
|
9
|
+
import { stainlessDocsSchemaExtension } from '../../schema-extension';
|
|
10
|
+
import { notFoundResponse, renderOptions } from '../utils';
|
|
11
|
+
|
|
12
|
+
type GetcollectionReturnType = Awaited<ReturnType<typeof getCollection<'docs'>>>[number];
|
|
13
|
+
|
|
14
|
+
type PageEntry = {
|
|
15
|
+
data: GetcollectionReturnType['data'] & z.infer<typeof stainlessDocsSchemaExtension>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
let contentEntriesCache: { id: string }[] | null = null;
|
|
19
|
+
|
|
20
|
+
async function getContentEntries(): Promise<{ id: string }[]> {
|
|
21
|
+
if (contentEntriesCache === null) {
|
|
22
|
+
contentEntriesCache = await getCollection('docs').then((entries) =>
|
|
23
|
+
entries.map((entry) => ({ id: entry.id })),
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
return contentEntriesCache ?? [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function resolveAutogenerateEntry(autogenerate: {
|
|
30
|
+
directory: string;
|
|
31
|
+
}): Promise<NonNullable<Tabs[0]['sidebar']>> {
|
|
32
|
+
const entries = await getContentEntries();
|
|
33
|
+
const directoryPrefix = autogenerate.directory.endsWith('/')
|
|
34
|
+
? autogenerate.directory
|
|
35
|
+
: `${autogenerate.directory}/`;
|
|
36
|
+
|
|
37
|
+
const matchingEntries = entries
|
|
38
|
+
.filter((entry) => {
|
|
39
|
+
return entry.id === autogenerate.directory || entry.id.startsWith(directoryPrefix);
|
|
40
|
+
})
|
|
41
|
+
.map((entry) => entry.id)
|
|
42
|
+
.sort();
|
|
43
|
+
|
|
44
|
+
return matchingEntries;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const findBreadcrumbsInSidebar = async (
|
|
48
|
+
data: NonNullable<Tabs[0]['sidebar']>,
|
|
49
|
+
slug: string,
|
|
50
|
+
): Promise<string[] | null> => {
|
|
51
|
+
for (const entry of data) {
|
|
52
|
+
if (typeof entry === 'string') {
|
|
53
|
+
if (entry === slug) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
if ('link' in entry && entry.link === slug) {
|
|
58
|
+
return [entry.label];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if ('autogenerate' in entry && entry.autogenerate) {
|
|
62
|
+
const resolvedItems = await resolveAutogenerateEntry(entry.autogenerate);
|
|
63
|
+
const breadcrumbs = await findBreadcrumbsInSidebar(resolvedItems, slug);
|
|
64
|
+
if (breadcrumbs) {
|
|
65
|
+
return breadcrumbs;
|
|
66
|
+
}
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const children = ('sidebar' in entry && entry.sidebar) || ('items' in entry && entry.items);
|
|
71
|
+
if (children) {
|
|
72
|
+
const breadcrumbs = await findBreadcrumbsInSidebar(children as NonNullable<Tabs[0]['sidebar']>, slug);
|
|
73
|
+
if (breadcrumbs) {
|
|
74
|
+
return [entry.label!, ...breadcrumbs];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const findBreadcrumbs = async (data: Tabs, slug: string): Promise<string[] | undefined> => {
|
|
83
|
+
for (const tab of data) {
|
|
84
|
+
if (tab.link === slug) {
|
|
85
|
+
return [tab.label];
|
|
86
|
+
}
|
|
87
|
+
if (!tab.sidebar) continue;
|
|
88
|
+
const breadcrumbs = await findBreadcrumbsInSidebar(tab.sidebar, slug);
|
|
89
|
+
if (breadcrumbs) {
|
|
90
|
+
return [tab.label, ...breadcrumbs.map((label) => label)];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
async function generateOgImage({ page, slug }: { page?: PageEntry; slug: string }) {
|
|
97
|
+
if (!page) {
|
|
98
|
+
return notFoundResponse();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const logoDataUrl = getLogoDataUrl({
|
|
102
|
+
logo: page.data.ogImageOptions?.logo,
|
|
103
|
+
theme: page.data.ogImageOptions?.theme || OG_IMAGE_OPTIONS?.theme,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const breadcrumbs = page.data.template !== 'splash' ? await findBreadcrumbs(TABS, slug) : undefined;
|
|
107
|
+
return new ImageResponse(
|
|
108
|
+
<OpenGraphImage
|
|
109
|
+
title={page.data.ogImageOptions?.title ?? page.data.title}
|
|
110
|
+
description={page.data.ogImageOptions?.description ?? page.data.description}
|
|
111
|
+
logo={logoDataUrl}
|
|
112
|
+
theme={page.data.ogImageOptions?.theme || OG_IMAGE_OPTIONS?.theme}
|
|
113
|
+
breadcrumbs={breadcrumbs}
|
|
114
|
+
/>,
|
|
115
|
+
renderOptions,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export default generateOgImage;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { LOGO, OG_IMAGE_OPTIONS } from 'virtual:stainless-docs/docs-og-image';
|
|
4
|
+
|
|
5
|
+
export function resolveLocalImageFile(logoPath: string): string | undefined {
|
|
6
|
+
try {
|
|
7
|
+
// Remove leading slash and resolve from project root
|
|
8
|
+
const filePath = join(process.cwd(), logoPath.replace(/^\//, ''));
|
|
9
|
+
const fileBuffer = readFileSync(filePath);
|
|
10
|
+
|
|
11
|
+
// Determine mime type from extension
|
|
12
|
+
const ext = logoPath.split('.').pop()?.toLowerCase();
|
|
13
|
+
|
|
14
|
+
const mimeType = ext === 'svg' ? 'image/svg+xml' : `image/${ext}`;
|
|
15
|
+
|
|
16
|
+
return `data:${mimeType};base64,${fileBuffer.toString('base64')}`;
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.warn('Failed to load logo for OG image:', error);
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Convert logo to base64 data URL if it exists
|
|
24
|
+
function getLogoDataUrl({ logo, theme }: { logo?: string; theme?: 'light' | 'dark' } = {}):
|
|
25
|
+
| string
|
|
26
|
+
| undefined {
|
|
27
|
+
const logoConfig = logo ?? OG_IMAGE_OPTIONS?.logo ?? LOGO;
|
|
28
|
+
if (!logoConfig) return undefined;
|
|
29
|
+
|
|
30
|
+
// Handle string path or object with src/light properties
|
|
31
|
+
let logoPath: string | undefined;
|
|
32
|
+
if (typeof logoConfig === 'string') {
|
|
33
|
+
logoPath = logoConfig;
|
|
34
|
+
} else if ('src' in logoConfig) {
|
|
35
|
+
logoPath = logoConfig.src;
|
|
36
|
+
} else if ('dark' in logoConfig && theme === 'dark') {
|
|
37
|
+
logoPath = logoConfig.dark;
|
|
38
|
+
} else if ('light' in logoConfig) {
|
|
39
|
+
logoPath = logoConfig.light;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!logoPath) return undefined;
|
|
43
|
+
|
|
44
|
+
return resolveLocalImageFile(logoPath);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default getLogoDataUrl;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { StarlightPlugin } from '@astrojs/starlight/types';
|
|
2
|
+
import type { NormalizedStainlessDocsConfig } from '../loadStlDocsConfig';
|
|
3
|
+
import { resolveSrcFile } from '../../resolveSrcFile';
|
|
4
|
+
import { resolve } from 'path';
|
|
5
|
+
import type { OGImageConfig } from './config';
|
|
6
|
+
import { buildVirtualModuleString } from '../../shared/virtualModule';
|
|
7
|
+
|
|
8
|
+
// The '\0' prefix tells Vite "this is a virtual module" and prevents it from being resolved again.
|
|
9
|
+
function resolveVirtualModuleId<T extends string>(id: T): `\0${T}` {
|
|
10
|
+
return `\0${id}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const OG_IMAGE_DIR = '/stl-docs/og-image';
|
|
14
|
+
|
|
15
|
+
const stainlessComponentDefaults = {
|
|
16
|
+
OpenGraphImage: resolveSrcFile(OG_IMAGE_DIR, 'components/OpenGraphImage.tsx'),
|
|
17
|
+
OpenGraphFunctionSignature: resolveSrcFile(OG_IMAGE_DIR, 'components/OpenGraphFunctionSignature.tsx'),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function checkTakumiInstalled(): boolean {
|
|
21
|
+
try {
|
|
22
|
+
import.meta.resolve('takumi-js/response');
|
|
23
|
+
return true;
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function ogImageStarlightPlugin(
|
|
30
|
+
config: OGImageConfig | undefined,
|
|
31
|
+
stainlessDocsConfig: NormalizedStainlessDocsConfig,
|
|
32
|
+
): StarlightPlugin {
|
|
33
|
+
return {
|
|
34
|
+
name: 'stainless-og-image',
|
|
35
|
+
hooks: {
|
|
36
|
+
'config:setup': ({ astroConfig, addRouteMiddleware, addIntegration, logger, command }) => {
|
|
37
|
+
if (command !== 'build' && command !== 'dev') {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!checkTakumiInstalled()) {
|
|
42
|
+
logger.error(
|
|
43
|
+
'The "takumi-js" package is required to use OG image generation. ' +
|
|
44
|
+
'Please install it: npm install takumi-js',
|
|
45
|
+
);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!astroConfig.site) {
|
|
50
|
+
logger.warn('astro.config.site is not set. Open Graph images will not be generated.');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
addRouteMiddleware({
|
|
54
|
+
entrypoint: resolveSrcFile(OG_IMAGE_DIR, 'routes/add-og-image.ts'),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
addIntegration({
|
|
58
|
+
name: 'stainless-docs-og-image-astro-integration',
|
|
59
|
+
hooks: {
|
|
60
|
+
'astro:config:setup': ({ updateConfig, injectRoute, command, config: astroConfig }) => {
|
|
61
|
+
const resolvePath = (id: string) =>
|
|
62
|
+
JSON.stringify(id.startsWith('.') ? resolve(astroConfig.root.pathname, id) : id);
|
|
63
|
+
|
|
64
|
+
const userComponents = Object.fromEntries(
|
|
65
|
+
Object.entries(config?.components ?? {}).flatMap(([key, value]) =>
|
|
66
|
+
value !== undefined ? [[key, value]] : [],
|
|
67
|
+
),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const allComponents: Record<string, string> = {
|
|
71
|
+
...stainlessComponentDefaults,
|
|
72
|
+
...userComponents,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const modules = Object.fromEntries(
|
|
76
|
+
Object.entries(allComponents).map(([name, path]) => [
|
|
77
|
+
`virtual:stainless-docs/docs-og-image/components/${name}`,
|
|
78
|
+
`export { default } from ${resolvePath(path)};`,
|
|
79
|
+
]),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const resolutionMap = Object.fromEntries(
|
|
83
|
+
Object.keys(modules).map((key) => [resolveVirtualModuleId(key), key]),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const virtualId = `virtual:stainless-docs/docs-og-image`;
|
|
87
|
+
|
|
88
|
+
updateConfig({
|
|
89
|
+
vite: {
|
|
90
|
+
plugins: [
|
|
91
|
+
{
|
|
92
|
+
name: '@stainless-api/docs-og-image-vite',
|
|
93
|
+
resolveId(id) {
|
|
94
|
+
if (id in modules || id == virtualId) {
|
|
95
|
+
return resolveVirtualModuleId(id);
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
load(id) {
|
|
99
|
+
if (id === resolveVirtualModuleId(virtualId)) {
|
|
100
|
+
return buildVirtualModuleString({
|
|
101
|
+
LOGO: stainlessDocsConfig?.starlightPassThrough?.logo,
|
|
102
|
+
OG_IMAGE_OPTIONS: config,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
const resolution = resolutionMap[id];
|
|
106
|
+
if (resolution) return modules[resolution];
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
injectRoute({
|
|
114
|
+
pattern: `/og/[...slug].png`,
|
|
115
|
+
entrypoint: resolveSrcFile(OG_IMAGE_DIR, 'routes/get-og-image.ts'),
|
|
116
|
+
prerender: command === 'build',
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (stainlessDocsConfig.apiReference !== null) {
|
|
120
|
+
const apiBasePath = stainlessDocsConfig.apiReference?.basePath ?? '/api';
|
|
121
|
+
const normalizedBasePath = apiBasePath.replace(/^\/+|\/+$/g, '');
|
|
122
|
+
|
|
123
|
+
injectRoute({
|
|
124
|
+
pattern: `/og/${normalizedBasePath}/[...slug].png`,
|
|
125
|
+
entrypoint: resolveSrcFile(OG_IMAGE_DIR, 'routes/get-api-reference-og-image.ts'),
|
|
126
|
+
prerender: command === 'build',
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { defineRouteMiddleware } from '@astrojs/starlight/route-data';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { OG_IMAGE_OPTIONS } from 'virtual:stainless-docs/docs-og-image';
|
|
4
|
+
import { renderOptions } from '../utils';
|
|
5
|
+
|
|
6
|
+
export const onRequest = defineRouteMiddleware((context) => {
|
|
7
|
+
const base = OG_IMAGE_OPTIONS?.basePath || '';
|
|
8
|
+
// Get the URL of the generated image for the current page using its ID and
|
|
9
|
+
// append the `.png` file extension.
|
|
10
|
+
const ogImageUrl = new URL(
|
|
11
|
+
path.posix.join(
|
|
12
|
+
import.meta.env.BASE_URL ?? '',
|
|
13
|
+
base,
|
|
14
|
+
'og',
|
|
15
|
+
`${context.locals.starlightRoute.id || 'index'}.png`,
|
|
16
|
+
),
|
|
17
|
+
context.site,
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
// Get the array of all tags to include in the `<head>` of the current page.
|
|
21
|
+
const { head } = context.locals.starlightRoute;
|
|
22
|
+
|
|
23
|
+
// Add the `<meta/>` tags for the Open Graph images.
|
|
24
|
+
head.push({
|
|
25
|
+
tag: 'meta',
|
|
26
|
+
attrs: { property: 'og:image', content: ogImageUrl.href },
|
|
27
|
+
});
|
|
28
|
+
head.push({
|
|
29
|
+
tag: 'meta',
|
|
30
|
+
attrs: { name: 'twitter:image', content: ogImageUrl.href },
|
|
31
|
+
});
|
|
32
|
+
head.push({
|
|
33
|
+
tag: 'meta',
|
|
34
|
+
attrs: { name: 'twitter:card', content: 'summary_large_image' },
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
head.push({
|
|
38
|
+
tag: 'meta',
|
|
39
|
+
attrs: { property: 'og:image:width', content: renderOptions.width.toString() },
|
|
40
|
+
});
|
|
41
|
+
head.push({
|
|
42
|
+
tag: 'meta',
|
|
43
|
+
attrs: { property: 'og:image:height', content: renderOptions.height.toString() },
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { generateAllDocsRoutes } from '@stainless-api/docs/generate-docs-routes';
|
|
2
|
+
import type { APIRoute } from 'astro';
|
|
3
|
+
import generateApiReferenceOgImage from '../image-gen/generate-api-reference-og-image';
|
|
4
|
+
import { notFoundResponse } from '../utils';
|
|
5
|
+
import { RESOLVED_API_REFERENCE_PATH } from 'virtual:stl-starlight-virtual-module';
|
|
6
|
+
|
|
7
|
+
const routes = await generateAllDocsRoutes();
|
|
8
|
+
|
|
9
|
+
export function getStaticPaths() {
|
|
10
|
+
return routes.map((route) => ({
|
|
11
|
+
params: { slug: `${route.params.slug}` },
|
|
12
|
+
...route.props,
|
|
13
|
+
}));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const GET: APIRoute = async ({ params }) => {
|
|
17
|
+
const slug = params?.slug;
|
|
18
|
+
if (!slug) return notFoundResponse();
|
|
19
|
+
|
|
20
|
+
// Remove slashes from the start and end of RESOLVED_API_REFERENCE_PATH
|
|
21
|
+
const apiBasePath = RESOLVED_API_REFERENCE_PATH?.replace(/^\/|\/$/g, '');
|
|
22
|
+
|
|
23
|
+
const slugWithoutBasePath = apiBasePath
|
|
24
|
+
? slug.startsWith(`${apiBasePath}/`)
|
|
25
|
+
? slug.slice(apiBasePath.length + 1)
|
|
26
|
+
: slug
|
|
27
|
+
: slug;
|
|
28
|
+
|
|
29
|
+
const apiReferenceRoute = routes.find((r) => r.params.slug === slugWithoutBasePath);
|
|
30
|
+
|
|
31
|
+
if (apiReferenceRoute) {
|
|
32
|
+
return generateApiReferenceOgImage({ apiReferenceRoute, slug });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return notFoundResponse();
|
|
36
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { getCollection } from 'astro:content';
|
|
2
|
+
import type { APIRoute } from 'astro';
|
|
3
|
+
import generateOgImage from '../image-gen/generate-og-image';
|
|
4
|
+
import { notFoundResponse } from '../utils';
|
|
5
|
+
|
|
6
|
+
const entries = await getCollection('docs');
|
|
7
|
+
// Map the entry array to an object with the page ID as key and the
|
|
8
|
+
// frontmatter data as value.
|
|
9
|
+
const pages = Object.fromEntries(entries.map(({ data, id }) => [id, { data }]));
|
|
10
|
+
|
|
11
|
+
export function getStaticPaths() {
|
|
12
|
+
const prosePaths = entries.map((entry) => ({
|
|
13
|
+
params: { slug: entry.id },
|
|
14
|
+
}));
|
|
15
|
+
return [...prosePaths];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const GET: APIRoute = async ({ params }) => {
|
|
19
|
+
const slug = params?.slug;
|
|
20
|
+
if (!slug) return notFoundResponse();
|
|
21
|
+
const page = pages[slug];
|
|
22
|
+
|
|
23
|
+
if (page) {
|
|
24
|
+
return generateOgImage({ slug, page });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return notFoundResponse();
|
|
28
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const lightThemeVars = {
|
|
2
|
+
background: '#ffffffff',
|
|
3
|
+
foreground: '#262626ff',
|
|
4
|
+
foregroundReduced: `#262626cc`,
|
|
5
|
+
foregroundMuted: `#26262666`,
|
|
6
|
+
green: '#16a34a',
|
|
7
|
+
greenBackground: '#16a34a14',
|
|
8
|
+
blue: '#155dfc',
|
|
9
|
+
blueBackground: '#155dfc14',
|
|
10
|
+
orange: '#ea580c',
|
|
11
|
+
orangeBackground: '#ea580c14',
|
|
12
|
+
red: '#d01e22',
|
|
13
|
+
redBackground: '#d01e2214',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const darkThemeVars = {
|
|
17
|
+
background: '#0a0a0aff',
|
|
18
|
+
foreground: '#ffffffff',
|
|
19
|
+
foregroundReduced: `#ffffffcc`,
|
|
20
|
+
foregroundMuted: `#ffffff66`,
|
|
21
|
+
green: '#22c55e',
|
|
22
|
+
greenBackground: '#22c55e27',
|
|
23
|
+
blue: '#2b7fff',
|
|
24
|
+
blueBackground: '#2b80ff27',
|
|
25
|
+
orange: '#f97316',
|
|
26
|
+
orangeBackground: '#f9731627',
|
|
27
|
+
red: '#d01e22',
|
|
28
|
+
redBackground: '#d01e2227',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const typography = {
|
|
32
|
+
baseFontSize: '40px',
|
|
33
|
+
baseLineHeight: '150%',
|
|
34
|
+
baseLetterSpacing: '-2%',
|
|
35
|
+
headerFontSize: '56px',
|
|
36
|
+
headerLineHeight: '120%',
|
|
37
|
+
headerLetterSpacing: '-3%',
|
|
38
|
+
breadcrumbFontSize: '32px',
|
|
39
|
+
breadcrumbLineHeight: '120%',
|
|
40
|
+
breadcrumbLetterSpacing: '-1%',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export { lightThemeVars, darkThemeVars, typography };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ImageResponseOptions } from 'takumi-js/response';
|
|
2
|
+
import { OG_IMAGE_OPTIONS } from 'virtual:stainless-docs/docs-og-image';
|
|
3
|
+
|
|
4
|
+
const defaultRenderOptions: ImageResponseOptions & { width: number; height: number } = {
|
|
5
|
+
width: 1200,
|
|
6
|
+
height: 630,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const renderOptions = {
|
|
10
|
+
...defaultRenderOptions,
|
|
11
|
+
...OG_IMAGE_OPTIONS?.renderOptions,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const notFoundResponse = () => new Response('Not found', { status: 404 });
|