@stainless-api/docs 0.1.0-beta.99 → 1.0.0-beta.141

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/CHANGELOG.md +401 -0
  2. package/ambient.d.ts +6 -0
  3. package/eslint-suppressions.json +22 -6
  4. package/{eslint.config.js → eslint.config.ts} +1 -7
  5. package/package.json +62 -40
  6. package/plugin/buildAlgoliaIndex.ts +6 -12
  7. package/plugin/components/SDKSelect.astro +0 -6
  8. package/plugin/components/SnippetCode.tsx +6 -37
  9. package/plugin/components/search/SearchAlgolia.astro +1 -1
  10. package/plugin/components/search/SearchIsland.tsx +19 -13
  11. package/plugin/generateAPIReferenceLink.ts +0 -40
  12. package/plugin/globalJs/ai-dropdown-options.ts +22 -9
  13. package/plugin/globalJs/code-snippets.ts +5 -5
  14. package/plugin/globalJs/copy.ts +20 -91
  15. package/plugin/globalJs/navigation.ts +13 -13
  16. package/plugin/globalJs/summary-selection-tweak.ts +29 -0
  17. package/plugin/index.ts +107 -163
  18. package/plugin/loadPluginConfig.ts +49 -151
  19. package/plugin/markdown/highlighter.ts +100 -0
  20. package/plugin/markdown/index.ts +39 -0
  21. package/plugin/middlewareBuilder/stainlessMiddleware.d.ts +2 -0
  22. package/plugin/react/Routing.tsx +10 -244
  23. package/plugin/referencePlaceholderUtils.ts +1 -1
  24. package/plugin/replaceSidebarPlaceholderMiddleware.ts +1 -1
  25. package/plugin/routes/Docs.astro +3 -1
  26. package/plugin/routes/Overview.astro +14 -7
  27. package/plugin/routes/llms.ts +186 -0
  28. package/plugin/routes/markdown.ts +62 -13
  29. package/plugin/sidebar-utils/sidebar-builder.ts +38 -12
  30. package/plugin/specs/defaultSpecLoader.ts +192 -0
  31. package/plugin/specs/fetchSpecSSR.ts +1 -1
  32. package/plugin/specs/utils.ts +86 -0
  33. package/shared/conditionalIntegration.ts +28 -0
  34. package/shared/getProsePages.ts +6 -7
  35. package/shared/virtualModule.ts +1 -26
  36. package/stl-docs/aiChatExamples.ts +31 -0
  37. package/stl-docs/chat/docs-chat-handler.ts +17 -0
  38. package/stl-docs/chat/hook.ts +225 -0
  39. package/stl-docs/chat/schemas.ts +27 -0
  40. package/stl-docs/chat/ui/AiChat.module.css +591 -0
  41. package/stl-docs/chat/ui/AiChat.tsx +175 -0
  42. package/stl-docs/chat/ui/Trigger.tsx +154 -0
  43. package/stl-docs/chat/ui/components/ChatControls.tsx +51 -0
  44. package/stl-docs/chat/ui/components/ChatEmpty.tsx +42 -0
  45. package/stl-docs/chat/ui/components/ChatLog.tsx +93 -0
  46. package/stl-docs/chat/ui/components/ChatMessage.tsx +47 -0
  47. package/stl-docs/chat/ui/components/CodeBlock.tsx +33 -0
  48. package/stl-docs/chat/ui/components/MessageFeedback.tsx +106 -0
  49. package/stl-docs/chat/ui/components/Table.tsx +15 -0
  50. package/stl-docs/chat/ui/components/ToolCall.tsx +34 -0
  51. package/stl-docs/chat/ui/components/hljs-github.css +81 -0
  52. package/stl-docs/chat/ui/scroll-manager.ts +86 -0
  53. package/stl-docs/chat/ui/types.ts +45 -0
  54. package/stl-docs/components/AiChatIsland.tsx +10 -12
  55. package/stl-docs/components/ContentPanel.astro +9 -0
  56. package/stl-docs/components/Footer.astro +89 -0
  57. package/stl-docs/components/Header.astro +0 -5
  58. package/stl-docs/components/PageFrame.astro +23 -8
  59. package/stl-docs/components/PageSidebar.astro +11 -0
  60. package/stl-docs/components/StainlessLogo.svg +4 -0
  61. package/stl-docs/components/TwoColumnContent.astro +2 -0
  62. package/stl-docs/components/headers/DefaultHeader.astro +6 -8
  63. package/stl-docs/components/headers/StackedHeader.astro +5 -53
  64. package/stl-docs/components/mintlify-compat/Accordion.astro +2 -2
  65. package/stl-docs/components/mintlify-compat/AccordionGroup.astro +0 -4
  66. package/stl-docs/components/mintlify-compat/Columns.astro +2 -2
  67. package/stl-docs/components/mintlify-compat/Frame.astro +2 -2
  68. package/stl-docs/components/mintlify-compat/Tab.astro +2 -2
  69. package/stl-docs/components/mintlify-compat/callouts/Callout.astro +2 -2
  70. package/stl-docs/components/mintlify-compat/callouts/Check.astro +0 -4
  71. package/stl-docs/components/mintlify-compat/callouts/Danger.astro +0 -4
  72. package/stl-docs/components/mintlify-compat/callouts/Info.astro +0 -4
  73. package/stl-docs/components/mintlify-compat/callouts/Note.astro +0 -4
  74. package/stl-docs/components/mintlify-compat/callouts/Tip.astro +0 -4
  75. package/stl-docs/components/mintlify-compat/callouts/Warning.astro +0 -4
  76. package/stl-docs/components/nav-tabs/NavDropdown.astro +12 -7
  77. package/stl-docs/components/nav-tabs/NavTabs.astro +5 -3
  78. package/stl-docs/components/nav-tabs/buildNavLinks.ts +2 -0
  79. package/stl-docs/components/pagination/Pagination.astro +4 -2
  80. package/stl-docs/components/pagination/PaginationLinkEmphasized.astro +2 -2
  81. package/stl-docs/components/pagination/PaginationLinkQuiet.astro +2 -2
  82. package/stl-docs/components/pagination/util.ts +3 -3
  83. package/stl-docs/components/sidebars/BaseSidebar.astro +72 -1
  84. package/stl-docs/disableCalloutSyntax.ts +1 -1
  85. package/stl-docs/fonts.ts +5 -5
  86. package/stl-docs/index.ts +76 -53
  87. package/stl-docs/loadStlDocsConfig.ts +38 -8
  88. package/stl-docs/og-image/components/OpenGraphFunctionSignature.tsx +64 -0
  89. package/stl-docs/og-image/components/OpenGraphImage.tsx +126 -0
  90. package/stl-docs/og-image/config.ts +56 -0
  91. package/stl-docs/og-image/image-gen/generate-api-reference-og-image.tsx +188 -0
  92. package/stl-docs/og-image/image-gen/generate-og-image.tsx +119 -0
  93. package/stl-docs/og-image/image-gen/get-logo-url.ts +47 -0
  94. package/stl-docs/og-image/index.ts +135 -0
  95. package/stl-docs/og-image/routes/add-og-image.ts +45 -0
  96. package/stl-docs/og-image/routes/get-api-reference-og-image.ts +36 -0
  97. package/stl-docs/og-image/routes/get-og-image.ts +28 -0
  98. package/stl-docs/og-image/theme.ts +43 -0
  99. package/stl-docs/og-image/utils.ts +14 -0
  100. package/stl-docs/proseDocSync.test.ts +74 -0
  101. package/stl-docs/proseDocSync.ts +344 -0
  102. package/stl-docs/proseMarkdown/proseMarkdownIntegration.ts +4 -12
  103. package/stl-docs/schema-extension.ts +12 -0
  104. package/stl-docs/tabsMiddleware.ts +1 -1
  105. package/styles/overrides.css +2 -14
  106. package/styles/page.css +210 -71
  107. package/styles/sidebar.css +30 -17
  108. package/styles/sl-variables.css +3 -8
  109. package/styles/stldocs-variables.css +2 -2
  110. package/styles/toc.css +8 -0
  111. package/tsconfig.json +1 -1
  112. package/virtual-module.d.ts +35 -11
  113. package/playground-virtual-modules.d.ts +0 -96
  114. package/plugin/globalJs/create-playground.shim.ts +0 -3
  115. package/plugin/globalJs/playground-data.shim.ts +0 -1
  116. package/plugin/globalJs/playground-data.ts +0 -14
  117. package/plugin/specs/FileCache.ts +0 -99
  118. package/plugin/specs/generateSpec.ts +0 -112
  119. package/plugin/specs/index.ts +0 -132
  120. package/plugin/specs/inputResolver.ts +0 -146
  121. package/plugin/specs/worker.ts +0 -199
  122. package/plugin/vendor/preview.worker.docs.js +0 -26108
  123. package/plugin/vendor/templates/cli.md +0 -1
  124. package/plugin/vendor/templates/go.md +0 -316
  125. package/plugin/vendor/templates/java.md +0 -89
  126. package/plugin/vendor/templates/kotlin.md +0 -89
  127. package/plugin/vendor/templates/node.md +0 -235
  128. package/plugin/vendor/templates/python.md +0 -251
  129. package/plugin/vendor/templates/ruby.md +0 -147
  130. package/plugin/vendor/templates/terraform.md +0 -60
  131. package/plugin/vendor/templates/typescript.md +0 -319
  132. package/scripts/vendor_deps.ts +0 -50
  133. package/stl-docs/components/ClientRouterHead.astro +0 -41
  134. package/stl-docs/components/content-panel/ContentPanel.astro +0 -42
  135. package/stl-docs/components/headers/SplashMobileMenuToggle.astro +0 -65
  136. 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 });