@clidey/dory 1.0.7 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -1
- package/bin/chat-server.ts +270 -0
- package/bin/dist/analytics-server.d.ts +11 -0
- package/bin/dist/analytics-server.d.ts.map +1 -0
- package/bin/dist/analytics-server.js +121 -0
- package/bin/dist/analytics-server.js.map +1 -0
- package/bin/dist/automation.d.ts +35 -0
- package/bin/dist/automation.d.ts.map +1 -0
- package/bin/dist/automation.js +259 -0
- package/bin/dist/automation.js.map +1 -0
- package/bin/dist/chat-server.d.ts +8 -0
- package/bin/dist/chat-server.d.ts.map +1 -0
- package/bin/dist/chat-server.js +322 -0
- package/bin/dist/chat-server.js.map +1 -0
- package/bin/dist/check.d.ts +16 -0
- package/bin/dist/check.d.ts.map +1 -0
- package/bin/dist/check.js +247 -0
- package/bin/dist/check.js.map +1 -0
- package/bin/dist/dory.js +252 -402
- package/bin/dist/dory.js.map +1 -1
- package/bin/dist/i18n-init.d.ts +6 -0
- package/bin/dist/i18n-init.d.ts.map +1 -0
- package/bin/dist/i18n-init.js +56 -0
- package/bin/dist/i18n-init.js.map +1 -0
- package/bin/dist/mcp-http.d.ts +8 -0
- package/bin/dist/mcp-http.d.ts.map +1 -0
- package/bin/dist/mcp-http.js +72 -0
- package/bin/dist/mcp-http.js.map +1 -0
- package/bin/dist/mcp.d.ts +2 -0
- package/bin/dist/mcp.d.ts.map +1 -0
- package/bin/dist/mcp.js +81 -0
- package/bin/dist/mcp.js.map +1 -0
- package/bin/dist/preview-entry.d.ts +2 -0
- package/bin/dist/preview-entry.d.ts.map +1 -0
- package/bin/dist/preview-entry.js +6 -0
- package/bin/dist/preview-entry.js.map +1 -0
- package/bin/dist/preview-server.d.ts +5 -0
- package/bin/dist/preview-server.d.ts.map +1 -0
- package/bin/dist/preview-server.js +159 -0
- package/bin/dist/preview-server.js.map +1 -0
- package/bin/dist/project-config.d.ts +22 -0
- package/bin/dist/project-config.d.ts.map +1 -0
- package/bin/dist/project-config.js +44 -0
- package/bin/dist/project-config.js.map +1 -0
- package/dist/index.html +57 -0
- package/dist/runtime/dory-runtime.css +2 -0
- package/dist/runtime/dory-runtime.html +14 -0
- package/dist/runtime/dory-runtime.js +5490 -0
- package/docs/api-playground-demo.mdx +119 -0
- package/docs/api-reference/asyncapi.json +324 -0
- package/docs/api-reference/openapi.json +366 -0
- package/docs/api-reference/operations/create.mdx +5 -0
- package/docs/api-reference/operations/delete.mdx +5 -0
- package/docs/api-reference/operations/get.mdx +5 -0
- package/docs/api-reference/operations/update.mdx +5 -0
- package/docs/api-reference/websocket-operations/receive-chat-message.mdx +5 -0
- package/docs/api-reference/websocket-operations/receive-notifications.mdx +5 -0
- package/docs/api-reference/websocket-operations/receive-presence-update.mdx +5 -0
- package/docs/api-reference/websocket-operations/send-chat-message.mdx +5 -0
- package/docs/api-reference/websocket-operations/send-presence-update.mdx +5 -0
- package/docs/assets/dory.jpeg +0 -0
- package/docs/components/accordion.mdx +145 -0
- package/docs/components/callouts.mdx +134 -0
- package/docs/components/cards.mdx +153 -0
- package/docs/components/code-blocks.mdx +281 -0
- package/docs/components/expandable.mdx +189 -0
- package/docs/components/icons.mdx +71 -0
- package/docs/components/images.mdx +82 -0
- package/docs/components/latex.mdx +148 -0
- package/docs/components/layout.mdx +175 -0
- package/docs/components/lists.mdx +134 -0
- package/docs/components/mermaid.mdx +149 -0
- package/docs/components/overview.mdx +81 -0
- package/docs/components/source.mdx +130 -0
- package/docs/components/steps.mdx +122 -0
- package/docs/components/tables.mdx +114 -0
- package/docs/components/tags.mdx +179 -0
- package/docs/components/typography.mdx +185 -0
- package/docs/configuration/analytics.mdx +45 -0
- package/docs/configuration/chat.mdx +59 -0
- package/docs/configuration/dory-json.mdx +278 -0
- package/docs/configuration/embed.mdx +73 -0
- package/docs/configuration/fonts.mdx +86 -0
- package/docs/configuration/internationalization.mdx +61 -0
- package/docs/configuration/navigation.mdx +217 -0
- package/docs/configuration/theming.mdx +166 -0
- package/docs/dory.json +129 -0
- package/docs/embed-example.html +176 -0
- package/docs/favicon.svg +18 -0
- package/docs/features.mdx +440 -0
- package/docs/getting-started/automation.mdx +76 -0
- package/docs/getting-started/configuration.mdx +95 -0
- package/docs/getting-started/installation.mdx +88 -0
- package/docs/getting-started/quick-start.mdx +53 -0
- package/docs/getting-started/whodb-ee-apps.mdx +122 -0
- package/docs/introduction.mdx +50 -0
- package/docs/logo/logo.svg +18 -0
- package/docs/playgrounds/api-playground.mdx +122 -0
- package/docs/playgrounds/websocket-playground.mdx +98 -0
- package/docs/social-card.png +0 -0
- package/docs/social-card.svg +17 -0
- package/docs/websocket-playground-demo.mdx +135 -0
- package/package.json +23 -17
- package/scripts/verify-runtime-parity.mjs +146 -0
- package/src/app.css +19 -1
- package/src/assets/fonts/HankenGrotesk.ttf +0 -0
- package/src/assets/fonts/OFL.txt +94 -0
- package/src/components/chat-assistant.tsx +229 -0
- package/src/components/dropdown.tsx +3 -3
- package/src/components/error-boundary.tsx +2 -2
- package/src/components/hooks.tsx +20 -1
- package/src/components/navigation.tsx +82 -51
- package/src/components/notification.tsx +2 -3
- package/src/components/routes.tsx +17 -19
- package/src/components/search-index.test.ts +15 -0
- package/src/components/search-index.ts +49 -48
- package/src/components/search.tsx +208 -104
- package/src/components/store.ts +27 -9
- package/src/config/mdx-common.ts +11 -0
- package/src/config/mdx.ts +10 -11
- package/src/embed/loader.ts +0 -3
- package/src/embed/types.ts +0 -2
- package/src/embed/widget-iframe.ts +19 -21
- package/src/embedded-runtime/config.ts +138 -0
- package/src/embedded-runtime/index.tsx +680 -0
- package/src/embedded-runtime/runtime.html +14 -0
- package/src/entry-server.tsx +46 -124
- package/src/main.tsx +16 -4
- package/src/mdx/accordion.tsx +3 -3
- package/src/mdx/api-playground.tsx +18 -16
- package/src/mdx/async-api.tsx +2 -2
- package/src/mdx/card.tsx +3 -2
- package/src/mdx/code.tsx +11 -9
- package/src/mdx/fence.tsx +6 -5
- package/src/mdx/icon-grid.tsx +4 -1
- package/src/mdx/icon-names.test.ts +15 -0
- package/src/mdx/icon-names.ts +60 -0
- package/src/mdx/icon-source-variants.ts +102 -0
- package/src/mdx/icon.tsx +12 -73
- package/src/mdx/latex.tsx +3 -2
- package/src/mdx/mdx.tsx +1 -1
- package/src/mdx/mermaid.tsx +12 -6
- package/src/mdx/open-api.tsx +130 -74
- package/src/mdx/preprocess.test.ts +16 -0
- package/src/mdx/preprocess.ts +61 -0
- package/src/mdx/processor.ts +4 -96
- package/src/mdx/source.tsx +5 -4
- package/src/mdx/specimen.tsx +38 -6
- package/src/mdx/tabs.tsx +29 -21
- package/src/mdx/text.tsx +9 -9
- package/src/mdx/try-it-panel.tsx +284 -0
- package/src/mdx/type-scale.tsx +4 -4
- package/src/mdx/websocket-playground.tsx +21 -14
- package/src/plugins/analytics-injector.ts +8 -0
- package/src/plugins/chat-dev-server.ts +32 -0
- package/src/plugins/docs-assets.test.ts +25 -0
- package/src/plugins/docs-assets.ts +6 -2
- package/src/plugins/frontmatter-dev-server.ts +2 -2
- package/src/plugins/frontmatter-generator.ts +1 -1
- package/src/plugins/html-files-middleware.test.ts +21 -1
- package/src/plugins/html-files-middleware.ts +26 -6
- package/src/plugins/html-metadata-injector.ts +24 -4
- package/src/plugins/llm-txt-dev-server.ts +2 -2
- package/src/plugins/llm-txt-generator.ts +21 -4
- package/src/plugins/prerender.ts +47 -20
- package/src/plugins/robots-generator.ts +1 -1
- package/src/plugins/sanitize.ts +1 -1
- package/src/plugins/shared.ts +6 -18
- package/src/plugins/sitemap-generator.ts +1 -1
- package/src/plugins/social-preview.test.ts +71 -0
- package/src/plugins/social-preview.ts +87 -0
- package/src/plugins/unified-mdx-plugin.ts +1 -1
- package/src/theme.scss +630 -207
- package/src/types/config.ts +53 -1
- package/src/ui/breadcrumb.tsx +17 -0
- package/src/ui/docs-header-view.tsx +32 -0
- package/src/ui/docs-navigation-view.tsx +69 -0
- package/src/ui/docs-page-frame.tsx +50 -0
- package/src/ui/dory-mark.tsx +9 -0
- package/src/ui/header.tsx +33 -90
- package/src/ui/layout.tsx +99 -153
- package/src/ui/mobile-navigation.tsx +10 -8
- package/src/ui/page-actions.tsx +67 -0
- package/src/ui/page-feedback.tsx +50 -0
- package/src/ui/page-footer.tsx +23 -0
- package/src/ui/prev-next-link.tsx +15 -69
- package/src/ui/prev-next-view.tsx +27 -0
- package/src/ui/table-of-content-view.tsx +167 -0
- package/src/ui/table-of-content.tsx +25 -183
- package/src/utils/analytics.test.ts +36 -0
- package/src/utils/analytics.ts +35 -0
- package/src/utils/colors.ts +7 -0
- package/src/utils/content-scope.test.ts +26 -0
- package/src/utils/content-scope.ts +39 -0
- package/src/utils/search-content.ts +12 -0
- package/src/utils/theme.ts +1 -7
- package/vite.config.ssr.ts +3 -3
- package/vite.config.ts +21 -15
package/src/plugins/shared.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import matter from 'gray-matter';
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import { normalizeBasePath } from '../utils/base-path';
|
|
4
|
+
import { normalizeBasePath } from '../utils/base-path.ts';
|
|
5
|
+
export { stripMdxToText } from '../utils/search-content.ts';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Shared helpers for build plugins that read docs/*.mdx and docs/dory.json.
|
|
@@ -62,6 +63,8 @@ export function getNavigationOrder(docsDir: string): string[] {
|
|
|
62
63
|
for (const page of pages) {
|
|
63
64
|
if (typeof page === 'string') {
|
|
64
65
|
order.push(page);
|
|
66
|
+
} else if (typeof page === 'object' && typeof page.page === 'string') {
|
|
67
|
+
order.push(page.page);
|
|
65
68
|
} else if (typeof page === 'object' && page.pages) {
|
|
66
69
|
walkPages(page.pages);
|
|
67
70
|
}
|
|
@@ -87,12 +90,13 @@ export function getNavigationOrder(docsDir: string): string[] {
|
|
|
87
90
|
* e.g. docs/getting-started/installation.mdx -> /getting-started/installation.
|
|
88
91
|
*/
|
|
89
92
|
export function pathFromFilename(filename: string, docsDir: string): string {
|
|
90
|
-
|
|
93
|
+
const route = filename
|
|
91
94
|
.replace(docsDir, '')
|
|
92
95
|
.replace(/\/?index\.mdx$/, '/')
|
|
93
96
|
.replace(/\.mdx$/, '')
|
|
94
97
|
.replace(/\\/g, '/')
|
|
95
98
|
.toLowerCase();
|
|
99
|
+
return route.length > 1 ? route.replace(/\/$/, '') : route;
|
|
96
100
|
}
|
|
97
101
|
|
|
98
102
|
/**
|
|
@@ -117,22 +121,6 @@ export function findMdxFilesMap(dir: string, baseDir: string): Record<string, st
|
|
|
117
121
|
return files;
|
|
118
122
|
}
|
|
119
123
|
|
|
120
|
-
/**
|
|
121
|
-
* Strips MDX/JSX syntax to extract plain text (for search indexing and
|
|
122
|
-
* meta description fallbacks).
|
|
123
|
-
*/
|
|
124
|
-
export function stripMdxToText(content: string): string {
|
|
125
|
-
return content
|
|
126
|
-
.replace(/^import\s+.*$/gm, '') // import statements
|
|
127
|
-
.replace(/^export\s+.*$/gm, '') // export statements
|
|
128
|
-
.replace(/```[\s\S]*?```/g, '') // fenced code blocks
|
|
129
|
-
.replace(/<[^>]+>/g, '') // JSX/HTML tags
|
|
130
|
-
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1') // markdown links -> text
|
|
131
|
-
.replace(/[#*_~`>|]/g, '') // markdown syntax chars
|
|
132
|
-
.replace(/\n{3,}/g, '\n\n') // collapse blank lines
|
|
133
|
-
.trim();
|
|
134
|
-
}
|
|
135
|
-
|
|
136
124
|
/**
|
|
137
125
|
* Escapes a string for safe use in HTML/XML text and attribute values.
|
|
138
126
|
*/
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { afterEach, expect, test } from 'vitest';
|
|
2
|
+
import { mkdtempSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
import { renderSocialPreview, socialPreviewPath, wrapSocialText } from './social-preview';
|
|
6
|
+
|
|
7
|
+
const folders: string[] = [];
|
|
8
|
+
afterEach(() => { for (const folder of folders.splice(0)) rmSync(folder, { recursive: true, force: true }); });
|
|
9
|
+
|
|
10
|
+
test('image paths distinguish nested routes and cannot escape the output directory', () => {
|
|
11
|
+
expect(socialPreviewPath('/guide/start')).not.toBe(socialPreviewPath('/reference/start'));
|
|
12
|
+
expect(socialPreviewPath('/../../private')).toMatch(/^\/_dory\/social\/page-[a-f0-9]+\.png$/);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('long titles wrap into bounded lines without cutting Unicode characters', () => {
|
|
16
|
+
const lines = wrapSocialText('A'.repeat(200), 30, 3);
|
|
17
|
+
expect(lines).toHaveLength(3);
|
|
18
|
+
expect(lines.every(line => Array.from(line).length <= 30)).toBe(true);
|
|
19
|
+
expect(lines[2]).toMatch(/…$/);
|
|
20
|
+
expect(wrapSocialText('😀'.repeat(12), 6, 2)).toEqual(['😀'.repeat(6), '😀'.repeat(6)]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('renders distinct 1200 by 630 PNGs with no system font or browser dependency', () => {
|
|
24
|
+
const root = mkdtempSync(resolve(tmpdir(), 'dory-social-'));
|
|
25
|
+
folders.push(root);
|
|
26
|
+
const docsDir = resolve(root, 'docs');
|
|
27
|
+
mkdirSync(docsDir);
|
|
28
|
+
const options = { config: { name: 'Example & Docs' }, docsDir, outDir: root, description: 'A guide for your team.' };
|
|
29
|
+
const first = renderSocialPreview({ ...options, route: '/install', title: 'Installation' });
|
|
30
|
+
const second = renderSocialPreview({ ...options, route: '/start', title: 'Getting started <safely>' });
|
|
31
|
+
const png = readFileSync(resolve(root, first.slice(1)));
|
|
32
|
+
expect(png.subarray(0, 8)).toEqual(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]));
|
|
33
|
+
expect(png.readUInt32BE(16)).toBe(1200);
|
|
34
|
+
expect(png.readUInt32BE(20)).toBe(630);
|
|
35
|
+
expect(png).not.toEqual(readFileSync(resolve(root, second.slice(1))));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('page overrides and shared-image opt-out produce correct base-prefixed metadata', async () => {
|
|
39
|
+
const { prerender } = await import('./prerender');
|
|
40
|
+
const { vi } = await import('vitest');
|
|
41
|
+
const { copyFileSync, writeFileSync } = await import('node:fs');
|
|
42
|
+
const root = mkdtempSync(resolve(tmpdir(), 'dory-social-metadata-'));
|
|
43
|
+
folders.push(root);
|
|
44
|
+
const dist = resolve(root, 'dist');
|
|
45
|
+
mkdirSync(resolve(root, 'docs'), { recursive: true });
|
|
46
|
+
mkdirSync(resolve(root, 'src/assets/fonts'), { recursive: true });
|
|
47
|
+
mkdirSync(dist);
|
|
48
|
+
copyFileSync(resolve('src/assets/fonts/HankenGrotesk.ttf'), resolve(root, 'src/assets/fonts/HankenGrotesk.ttf'));
|
|
49
|
+
const config = { name: 'Example', url: 'https://docs.example.com', basePath: '/manual', image: '/shared.png', navigation: { tabs: [] } };
|
|
50
|
+
const template = '<html lang="en"><head><title>Docs</title><meta name="description" content="" /><meta property="og:image" content="" /><meta name="twitter:image" content="" /><meta name="twitter:card" content="summary" /></head><body><div id="app"></div></body></html>';
|
|
51
|
+
writeFileSync(resolve(dist, 'frontmatter.json'), JSON.stringify([
|
|
52
|
+
{ path: '/start', title: 'Start', description: 'Get started.' },
|
|
53
|
+
{ path: '/custom', title: 'Custom', description: 'Custom page.', image: '/art/custom.png' },
|
|
54
|
+
]));
|
|
55
|
+
const cwd = vi.spyOn(process, 'cwd').mockReturnValue(root);
|
|
56
|
+
try {
|
|
57
|
+
for (const enabled of [true, false]) {
|
|
58
|
+
writeFileSync(resolve(root, 'docs/dory.json'), JSON.stringify({ ...config, seo: { generateSocialImages: enabled } }));
|
|
59
|
+
writeFileSync(resolve(dist, 'index.html'), template);
|
|
60
|
+
const hooks = prerender() as { configResolved(config: unknown): void; closeBundle(): void };
|
|
61
|
+
hooks.configResolved({ build: { outDir: dist } });
|
|
62
|
+
hooks.closeBundle();
|
|
63
|
+
const start = readFileSync(resolve(dist, 'start/index.html'), 'utf8');
|
|
64
|
+
const custom = readFileSync(resolve(dist, 'custom/index.html'), 'utf8');
|
|
65
|
+
const image = enabled ? socialPreviewPath('/start') : '/shared.png';
|
|
66
|
+
expect(start).toContain(`content="https://docs.example.com/manual${image}"`);
|
|
67
|
+
expect(custom).toContain('content="https://docs.example.com/manual/art/custom.png"');
|
|
68
|
+
if (enabled) expect(readFileSync(resolve(dist, image.slice(1))).readUInt32BE(16)).toBe(1200);
|
|
69
|
+
}
|
|
70
|
+
} finally { cwd.mockRestore(); }
|
|
71
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, extname, isAbsolute, relative, resolve } from 'node:path';
|
|
4
|
+
import { Resvg } from '@resvg/resvg-js';
|
|
5
|
+
import type { DoryConfig } from '../types/config.ts';
|
|
6
|
+
import { escapeHtml } from './shared.ts';
|
|
7
|
+
|
|
8
|
+
export function socialPreviewPath(route: string): string {
|
|
9
|
+
return route === '/' ? '/_dory/social/home.png' : `/_dory/social/page-${createHash('sha256').update(route).digest('hex').slice(0, 20)}.png`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function localAsset(docsDir: string, source?: string): string | undefined {
|
|
13
|
+
if (!source || /^(?:[a-z]+:|\/\/)/i.test(source)) return;
|
|
14
|
+
const file = resolve(docsDir, source.replace(/^\//, ''));
|
|
15
|
+
if (!existsSync(file)) return;
|
|
16
|
+
const rel = relative(realpathSync(docsDir), realpathSync(file));
|
|
17
|
+
if (rel.startsWith('..') || isAbsolute(rel)) return;
|
|
18
|
+
return file;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Bounded lines, including long words and titles without spaces. */
|
|
22
|
+
export function wrapSocialText(text: string, columns: number, maxLines: number): string[] {
|
|
23
|
+
const words = text.replace(/\s+/g, ' ').trim().split(' ');
|
|
24
|
+
const lines: string[] = [];
|
|
25
|
+
let line = '';
|
|
26
|
+
for (const word of words) {
|
|
27
|
+
if (line && Array.from(`${line} ${word}`).length > columns) { lines.push(line); line = ''; }
|
|
28
|
+
const characters = Array.from(word);
|
|
29
|
+
while (characters.length > columns) {
|
|
30
|
+
if (line) { lines.push(line); line = ''; }
|
|
31
|
+
lines.push(characters.splice(0, columns).join(''));
|
|
32
|
+
}
|
|
33
|
+
const tail = characters.join('');
|
|
34
|
+
if (tail) line = line ? `${line} ${tail}` : tail;
|
|
35
|
+
}
|
|
36
|
+
if (line) lines.push(line);
|
|
37
|
+
if (lines.length > maxLines) {
|
|
38
|
+
lines.length = maxLines;
|
|
39
|
+
lines[maxLines - 1] = `${Array.from(lines[maxLines - 1]).slice(0, columns - 1).join('').trimEnd()}…`;
|
|
40
|
+
}
|
|
41
|
+
return lines;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Build-only PNG generation: local files, bundled font, no browser or network. */
|
|
45
|
+
export function renderSocialPreview({ route, title, description, config, docsDir, outDir }: {
|
|
46
|
+
route: string; title: string; description: string; config: Partial<DoryConfig>; docsDir: string; outDir: string;
|
|
47
|
+
}): string {
|
|
48
|
+
const name = config.name || 'Documentation';
|
|
49
|
+
const hex = (value: string | undefined, fallback: string) => value && /^#[a-f\d]{3,8}$/i.test(value) ? value : fallback;
|
|
50
|
+
const background = hex(config.colors?.background, '#15243e');
|
|
51
|
+
const primary = hex(config.colors?.primary, '#5c8cf4');
|
|
52
|
+
const foreground = hex(config.colors?.foreground, '#f8f5ed');
|
|
53
|
+
const muted = hex(config.colors?.mutedForeground, '#b2bdd0');
|
|
54
|
+
const logo = localAsset(docsDir, config.logo?.dark ?? config.logo?.light);
|
|
55
|
+
const imageTypes: Record<string, string> = { '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.webp': 'image/webp' };
|
|
56
|
+
const logoType = logo && imageTypes[extname(logo).toLowerCase()];
|
|
57
|
+
const logoData = logo && logoType ? `data:${logoType};base64,${readFileSync(logo).toString('base64')}` : undefined;
|
|
58
|
+
const font = localAsset(docsDir, config.fonts?.heading?.source ?? config.fonts?.source);
|
|
59
|
+
const customFont = font && /\.(ttf|otf)$/i.test(font) ? font : undefined;
|
|
60
|
+
const family = customFont ? config.fonts?.heading?.family ?? config.fonts?.family ?? 'Hanken Grotesk' : 'Hanken Grotesk';
|
|
61
|
+
const size = title.length > 85 ? 56 : 64;
|
|
62
|
+
const lines = wrapSocialText(title, Math.floor(1050 / (size * 0.62)), 3);
|
|
63
|
+
const text = (value: string, x: number, y: number, fontSize: number, color: string, weight = 400) => `<text x="${x}" y="${y}" font-size="${fontSize}" fill="${color}" font-weight="${weight}">${escapeHtml(value)}</text>`;
|
|
64
|
+
const subtitleY = Math.max(350, 225 + (lines.length - 1) * 76 + 65);
|
|
65
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630">
|
|
66
|
+
<rect width="1200" height="630" fill="${background}"/><rect width="1200" height="8" fill="${primary}"/>
|
|
67
|
+
<circle cx="1170" cy="360" r="280" fill="${primary}" opacity="0.06"/>
|
|
68
|
+
<g font-family="${escapeHtml(family)}, sans-serif">
|
|
69
|
+
${logoData ? `<image href="${logoData}" x="72" y="62" width="46" height="50"/>` : `<rect x="72" y="64" width="44" height="44" rx="10" fill="${primary}"/>`}
|
|
70
|
+
${text(wrapSocialText(name, 55, 1)[0] ?? '', 136, 100, 32, foreground, 700)}
|
|
71
|
+
${lines.map((line, i) => text(line, 72, 225 + i * 76, size, foreground, 700)).join('')}
|
|
72
|
+
${wrapSocialText(description, 78, 2).map((line, i) => text(line, 76, subtitleY + i * 36, 26, muted)).join('')}
|
|
73
|
+
<path d="M76 518H1124" stroke="${foreground}" opacity="0.15"/>
|
|
74
|
+
${text('DOCUMENTATION', 76, 565, 17, muted)}
|
|
75
|
+
<text x="1124" y="565" text-anchor="end" font-size="20" fill="${primary}">${escapeHtml(wrapSocialText(config.url?.replace(/^https?:\/\//, '') ?? name, 60, 1)[0] ?? '')}</text>
|
|
76
|
+
</g></svg>`;
|
|
77
|
+
const path = socialPreviewPath(route);
|
|
78
|
+
const file = resolve(outDir, path.slice(1));
|
|
79
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
80
|
+
const renderer = new Resvg(svg, { font: {
|
|
81
|
+
loadSystemFonts: false,
|
|
82
|
+
fontFiles: [resolve(process.cwd(), 'src/assets/fonts/HankenGrotesk.ttf'), ...(customFont ? [customFont] : [])],
|
|
83
|
+
defaultFontFamily: family,
|
|
84
|
+
} });
|
|
85
|
+
writeFileSync(file, renderer.render().asPng());
|
|
86
|
+
return path;
|
|
87
|
+
}
|