@clidey/dory 1.0.5 → 1.0.6
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/bin/dist/dory.js +36 -2
- package/bin/dist/dory.js.map +1 -1
- package/index.html +0 -1
- package/package.json +16 -22
- package/src/components/hooks.tsx +2 -1
- package/src/components/store.ts +5 -4
- package/src/config/mdx.js +8 -3
- package/src/config/mdx.ts +8 -3
- package/src/embed/widget-iframe.ts +10 -3
- package/src/entry-server.tsx +11 -7
- package/src/main.tsx +5 -1
- package/src/mdx/accordion.tsx +11 -8
- package/src/mdx/api.tsx +2 -352
- package/src/mdx/async-api.tsx +124 -295
- package/src/mdx/callout.tsx +22 -24
- package/src/mdx/card.tsx +20 -22
- package/src/mdx/chip.tsx +14 -0
- package/src/mdx/code.tsx +133 -110
- package/src/mdx/expandable.tsx +8 -8
- package/src/mdx/fence-meta.test.ts +39 -0
- package/src/mdx/fence-meta.ts +61 -0
- package/src/mdx/fence.tsx +155 -44
- package/src/mdx/icon-grid.tsx +29 -0
- package/src/mdx/icon.tsx +2 -2
- package/src/mdx/latex.tsx +14 -7
- package/src/mdx/list.tsx +3 -3
- package/src/mdx/mdx.tsx +7 -1
- package/src/mdx/mermaid.tsx +81 -11
- package/src/mdx/open-api.tsx +396 -239
- package/src/mdx/source.tsx +148 -40
- package/src/mdx/specimen.tsx +69 -0
- package/src/mdx/step.tsx +22 -30
- package/src/mdx/table.tsx +4 -9
- package/src/mdx/tabs.tsx +39 -0
- package/src/mdx/tag.tsx +13 -21
- package/src/mdx/text.tsx +17 -9
- package/src/mdx/type-scale.tsx +28 -0
- package/src/mdx/websocket-playground.tsx +221 -704
- package/src/plugins/code-meta.ts +23 -0
- package/src/plugins/frontmatter-dev-server.ts +6 -2
- package/src/plugins/html-files-middleware.ts +9 -2
- package/src/plugins/html-metadata-injector.ts +4 -3
- package/src/plugins/llm-txt-dev-server.ts +5 -2
- package/src/plugins/llm-txt-generator.ts +2 -1
- package/src/plugins/prerender.ts +7 -4
- package/src/plugins/relative-assets.test.ts +26 -0
- package/src/plugins/relative-assets.ts +80 -0
- package/src/plugins/robots-generator.ts +6 -4
- package/src/plugins/sanitize.test.ts +5 -0
- package/src/plugins/sanitize.ts +2 -0
- package/src/plugins/shared.ts +17 -0
- package/src/plugins/sitemap-generator.ts +2 -1
- package/src/theme.scss +282 -3
- package/src/types/config.ts +3 -0
- package/src/ui/header.tsx +4 -3
- package/src/ui/layout.tsx +22 -4
- package/src/ui/page-meta.tsx +37 -0
- package/src/ui/prev-next-link.tsx +4 -3
- package/src/ui/table-of-content.tsx +11 -3
- package/src/utils/base-path.test.ts +46 -0
- package/src/utils/base-path.ts +41 -0
- package/vite.config.ssr.ts +3 -0
- package/vite.config.ts +3 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { visit } from 'unist-util-visit';
|
|
2
|
+
|
|
3
|
+
interface CodeElement {
|
|
4
|
+
tagName?: string;
|
|
5
|
+
data?: { meta?: string };
|
|
6
|
+
properties?: Record<string, unknown>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Rehype plugin that keeps a code fence's meta string (everything after the
|
|
11
|
+
* language, e.g. ```json dory.json {4-6}). MDX drops it by default; this
|
|
12
|
+
* exposes it to the `code` component as the `metastring` prop.
|
|
13
|
+
*/
|
|
14
|
+
export function rehypeCodeMeta() {
|
|
15
|
+
return (tree: Parameters<typeof visit>[0]) => {
|
|
16
|
+
visit(tree, 'element', (node: CodeElement) => {
|
|
17
|
+
const meta = node.data?.meta;
|
|
18
|
+
if (node.tagName === 'code' && meta) {
|
|
19
|
+
node.properties = { ...node.properties, metastring: meta };
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Plugin } from 'vite';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { generateFrontmatterJson, generateSearchContentJson } from './frontmatter-generator';
|
|
4
|
+
import { getBasePath, readDoryConfig } from './shared';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Dev server plugin that serves frontmatter.json and search-content.json
|
|
@@ -10,8 +11,11 @@ export function frontmatterDevServer(): Plugin {
|
|
|
10
11
|
return {
|
|
11
12
|
name: 'frontmatter-dev-server',
|
|
12
13
|
configureServer(server) {
|
|
14
|
+
// Plugin middlewares run before Vite strips `base`, so mount under it.
|
|
15
|
+
const basePath = getBasePath(readDoryConfig());
|
|
16
|
+
|
|
13
17
|
// Serve search content for client-side indexing
|
|
14
|
-
server.middlewares.use(
|
|
18
|
+
server.middlewares.use(`${basePath}/search-content.json`, (req, res, next) => {
|
|
15
19
|
if (req.method !== 'GET') return next();
|
|
16
20
|
try {
|
|
17
21
|
const docsDir = path.join(process.cwd(), 'docs');
|
|
@@ -27,7 +31,7 @@ export function frontmatterDevServer(): Plugin {
|
|
|
27
31
|
}
|
|
28
32
|
});
|
|
29
33
|
|
|
30
|
-
server.middlewares.use(
|
|
34
|
+
server.middlewares.use(`${basePath}/frontmatter.json`, (req, res, next) => {
|
|
31
35
|
if (req.method !== 'GET') {
|
|
32
36
|
return next();
|
|
33
37
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Plugin } from 'vite';
|
|
2
2
|
import { readFileSync, existsSync, statSync } from 'fs';
|
|
3
3
|
import { resolve, extname, sep } from 'path';
|
|
4
|
+
import { getBasePath, readDoryConfig } from './shared';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Returns true if `target` is the `root` path itself or is contained within it.
|
|
@@ -50,14 +51,20 @@ export function htmlFilesMiddleware(): Plugin {
|
|
|
50
51
|
return {
|
|
51
52
|
name: 'html-files-middleware',
|
|
52
53
|
configureServer(server) {
|
|
54
|
+
// Plugin middlewares run before Vite strips `base`, so strip it here.
|
|
55
|
+
const basePath = getBasePath(readDoryConfig());
|
|
56
|
+
|
|
53
57
|
server.middlewares.use((req, res, next) => {
|
|
54
58
|
if (!req.url) {
|
|
55
59
|
next();
|
|
56
60
|
return;
|
|
57
61
|
}
|
|
58
62
|
|
|
59
|
-
// Remove query params
|
|
60
|
-
|
|
63
|
+
// Remove query params and the base path prefix
|
|
64
|
+
let url = req.url.split('?')[0];
|
|
65
|
+
if (basePath && (url === basePath || url.startsWith(`${basePath}/`))) {
|
|
66
|
+
url = url.slice(basePath.length) || '/';
|
|
67
|
+
}
|
|
61
68
|
|
|
62
69
|
// Check if the request is for a file in the dist directory
|
|
63
70
|
if (url.startsWith('/dist/')) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Plugin } from 'vite';
|
|
2
|
-
import { readDoryConfig, escapeHtml, applyTitleTemplate } from './shared';
|
|
2
|
+
import { readDoryConfig, escapeHtml, applyTitleTemplate, getBasePath, getSiteUrl } from './shared';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Vite plugin that injects site metadata from dory.json into index.html.
|
|
@@ -20,7 +20,8 @@ export function htmlMetadataInjector(): Plugin {
|
|
|
20
20
|
const siteName: string = config.name || 'Documentation';
|
|
21
21
|
const defaultTitle = applyTitleTemplate(config.title || siteName, seo.titleTemplate, siteName);
|
|
22
22
|
const defaultDescription: string = config.description || `${siteName} - Technical Documentation`;
|
|
23
|
-
const siteUrl: string = config
|
|
23
|
+
const siteUrl: string = getSiteUrl(config);
|
|
24
|
+
const basePath = getBasePath(config);
|
|
24
25
|
const twitterHandle: string = config.twitter || '';
|
|
25
26
|
|
|
26
27
|
// Resolve image to absolute URL
|
|
@@ -33,7 +34,7 @@ export function htmlMetadataInjector(): Plugin {
|
|
|
33
34
|
// fall back to the logo so sites without a dedicated favicon still
|
|
34
35
|
// get an icon instead of a broken link.
|
|
35
36
|
const rawFavicon: string = config.favicon || config.logo?.light || config.logo?.dark || '';
|
|
36
|
-
const faviconHref = rawFavicon ?
|
|
37
|
+
const faviconHref = rawFavicon ? `${basePath}/${rawFavicon.replace(/^\.?\/*/, '')}` : '';
|
|
37
38
|
|
|
38
39
|
// Function replacements everywhere so `$&` etc. in config values
|
|
39
40
|
// cannot corrupt the output.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Plugin } from 'vite';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { generateLlmsIndex, generateLlmContent } from './llm-txt-generator';
|
|
4
|
+
import { getBasePath, readDoryConfig } from './shared';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Dev server plugin that serves llms.txt and llms-full.txt on demand,
|
|
@@ -30,8 +31,10 @@ export function llmTxtDevServer(): Plugin {
|
|
|
30
31
|
return {
|
|
31
32
|
name: 'llm-txt-dev-server',
|
|
32
33
|
configureServer(server) {
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
// Plugin middlewares run before Vite strips `base`, so mount under it.
|
|
35
|
+
const basePath = getBasePath(readDoryConfig());
|
|
36
|
+
server.middlewares.use(`${basePath}/llms.txt`, serveText(generateLlmsIndex));
|
|
37
|
+
server.middlewares.use(`${basePath}/llms-full.txt`, serveText(generateLlmContent));
|
|
35
38
|
}
|
|
36
39
|
};
|
|
37
40
|
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
parseFrontmatter,
|
|
8
8
|
findMdxFilesMap,
|
|
9
9
|
pathFromFilename,
|
|
10
|
+
getSiteUrl,
|
|
10
11
|
} from './shared';
|
|
11
12
|
|
|
12
13
|
interface LlmTxtGeneratorOptions {
|
|
@@ -41,7 +42,7 @@ function resolveFileKey(navKey: string, mdxFilesMap: Record<string, string>): st
|
|
|
41
42
|
export function generateLlmsIndex(docsDir: string): string {
|
|
42
43
|
const config = readDoryConfig(docsDir) || {};
|
|
43
44
|
const siteName: string = config.name || 'Documentation';
|
|
44
|
-
const baseUrl: string = config
|
|
45
|
+
const baseUrl: string = getSiteUrl(config);
|
|
45
46
|
const mdxFilesMap = findMdxFilesMap(docsDir, docsDir);
|
|
46
47
|
|
|
47
48
|
let output = `# ${siteName}\n`;
|
package/src/plugins/prerender.ts
CHANGED
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
stripMdxToText,
|
|
10
10
|
escapeHtml,
|
|
11
11
|
applyTitleTemplate,
|
|
12
|
+
getBasePath,
|
|
13
|
+
getSiteUrl,
|
|
12
14
|
} from './shared';
|
|
13
15
|
|
|
14
16
|
/**
|
|
@@ -41,7 +43,7 @@ export function prerender(): Plugin {
|
|
|
41
43
|
|
|
42
44
|
const siteConfig = readDoryConfig() || {};
|
|
43
45
|
const seo = siteConfig.seo || {};
|
|
44
|
-
const baseUrl: string = siteConfig
|
|
46
|
+
const baseUrl: string = getSiteUrl(siteConfig);
|
|
45
47
|
const siteName: string = siteConfig.name || 'Documentation';
|
|
46
48
|
|
|
47
49
|
if (!baseUrl) {
|
|
@@ -209,7 +211,7 @@ export function prerender(): Plugin {
|
|
|
209
211
|
);
|
|
210
212
|
}
|
|
211
213
|
|
|
212
|
-
write404(outDir, baseHtml, siteName, frontmatter);
|
|
214
|
+
write404(outDir, baseHtml, siteName, frontmatter, getBasePath(siteConfig));
|
|
213
215
|
|
|
214
216
|
console.log(`✅ Prerendered ${count} routes with page-specific meta tags`);
|
|
215
217
|
} catch (error) {
|
|
@@ -241,10 +243,11 @@ function write404(
|
|
|
241
243
|
outDir: string,
|
|
242
244
|
baseHtml: string,
|
|
243
245
|
siteName: string,
|
|
244
|
-
frontmatter: Array<Record<string, any
|
|
246
|
+
frontmatter: Array<Record<string, any>>,
|
|
247
|
+
basePath: string
|
|
245
248
|
): void {
|
|
246
249
|
const firstPage = frontmatter.find(page => page.path && page.path !== '/');
|
|
247
|
-
const firstHref: string = firstPage?.path || '/'
|
|
250
|
+
const firstHref: string = `${basePath}${firstPage?.path || '/'}`;
|
|
248
251
|
const firstTitle: string = firstPage?.title || siteName;
|
|
249
252
|
|
|
250
253
|
let html = baseHtml;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { resolveAssetUrl } from './relative-assets';
|
|
3
|
+
|
|
4
|
+
const docsDir = '/site/docs';
|
|
5
|
+
|
|
6
|
+
describe('resolveAssetUrl', () => {
|
|
7
|
+
it('resolves against the MDX file directory', () => {
|
|
8
|
+
expect(resolveAssetUrl('./assets/a.png', '/site/docs/features.mdx', docsDir)).toBe('/assets/a.png');
|
|
9
|
+
expect(resolveAssetUrl('../assets/a.png', '/site/docs/components/images.mdx', docsDir)).toBe('/assets/a.png');
|
|
10
|
+
expect(resolveAssetUrl('shots/b.png', '/site/docs/guides/intro.mdx', docsDir)).toBe('/guides/shots/b.png');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('keeps query strings and fragments', () => {
|
|
14
|
+
expect(resolveAssetUrl('./a.svg?v=2#icon', '/site/docs/page.mdx', docsDir)).toBe('/a.svg?v=2#icon');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('leaves absolute, protocol and anchor URLs alone', () => {
|
|
18
|
+
for (const url of ['/assets/a.png', 'https://x.dev/a.png', '//cdn.dev/a.png', 'data:image/png;base64,AA', '#top']) {
|
|
19
|
+
expect(resolveAssetUrl(url, '/site/docs/page.mdx', docsDir)).toBeNull();
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('leaves paths that escape the docs directory alone', () => {
|
|
24
|
+
expect(resolveAssetUrl('../../outside.png', '/site/docs/components/x.mdx', docsDir)).toBeNull();
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { dirname, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { visit } from 'unist-util-visit';
|
|
4
|
+
|
|
5
|
+
interface ImageNode {
|
|
6
|
+
type: 'image';
|
|
7
|
+
url: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface JsxElementNode {
|
|
11
|
+
type: 'mdxJsxFlowElement' | 'mdxJsxTextElement';
|
|
12
|
+
name: string | null;
|
|
13
|
+
attributes: Array<{ type: string; name?: string; value?: unknown }>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Absolute paths, protocol URLs (https:, data:, mailto:), protocol-relative URLs and anchors stay as written.
|
|
17
|
+
const notRelative = /^(?:[a-z][a-z\d+.-]*:|\/|#)/i;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolves a relative asset URL written in `mdxFile` to a root path on the
|
|
21
|
+
* built site (docs/ maps to /). Returns null for URLs that are not relative
|
|
22
|
+
* or that point outside the docs directory.
|
|
23
|
+
*/
|
|
24
|
+
export function resolveAssetUrl(url: string, mdxFile: string, docsDir: string): string | null {
|
|
25
|
+
if (!url || notRelative.test(url)) return null;
|
|
26
|
+
|
|
27
|
+
const suffixIndex = url.search(/[?#]/);
|
|
28
|
+
const pathPart = suffixIndex === -1 ? url : url.slice(0, suffixIndex);
|
|
29
|
+
const suffix = suffixIndex === -1 ? '' : url.slice(suffixIndex);
|
|
30
|
+
|
|
31
|
+
const target = resolve(dirname(mdxFile), pathPart);
|
|
32
|
+
const fromDocs = relative(docsDir, target);
|
|
33
|
+
if (fromDocs.startsWith('..') || fromDocs === '') return null;
|
|
34
|
+
|
|
35
|
+
return `/${fromDocs.split(sep).join('/')}${suffix}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Remark plugin that rewrites relative image paths to root paths, resolved
|
|
40
|
+
* against the MDX file's own directory. Pages are served as directories
|
|
41
|
+
* (/features/ after a trailing-slash redirect), so a relative path left in
|
|
42
|
+
* the HTML would resolve against the page URL instead of the file.
|
|
43
|
+
* Covers Markdown images and <img src="..."> in MDX, and warns when the
|
|
44
|
+
* target file does not exist.
|
|
45
|
+
*/
|
|
46
|
+
export function remarkRelativeAssets(options: { docsDir?: string } = {}) {
|
|
47
|
+
const docsDir = options.docsDir ?? resolve(process.cwd(), 'docs');
|
|
48
|
+
|
|
49
|
+
return (tree: Parameters<typeof visit>[0], file: { path?: string }) => {
|
|
50
|
+
const mdxFile = file.path;
|
|
51
|
+
if (!mdxFile) return;
|
|
52
|
+
|
|
53
|
+
const rewrite = (url: string): string | null => {
|
|
54
|
+
const resolved = resolveAssetUrl(url, mdxFile, docsDir);
|
|
55
|
+
if (resolved === null) return null;
|
|
56
|
+
const onDisk = resolve(docsDir, decodeURI(resolved.replace(/[?#].*$/, '')).slice(1));
|
|
57
|
+
if (!existsSync(onDisk)) {
|
|
58
|
+
console.warn(`⚠️ ${relative(docsDir, mdxFile)}: image "${url}" not found at ${relative(process.cwd(), onDisk)}`);
|
|
59
|
+
}
|
|
60
|
+
return resolved;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
visit(tree, (node) => {
|
|
64
|
+
if (node.type === 'image') {
|
|
65
|
+
const image = node as unknown as ImageNode;
|
|
66
|
+
image.url = rewrite(image.url) ?? image.url;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement') {
|
|
71
|
+
const element = node as unknown as JsxElementNode;
|
|
72
|
+
if (element.name !== 'img') return;
|
|
73
|
+
const src = element.attributes.find((attr) => attr.type === 'mdxJsxAttribute' && attr.name === 'src');
|
|
74
|
+
if (src && typeof src.value === 'string') {
|
|
75
|
+
src.value = rewrite(src.value) ?? src.value;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Plugin } from 'vite';
|
|
2
|
-
import { readDoryConfig } from './shared';
|
|
2
|
+
import { readDoryConfig, getBasePath, getSiteUrl } from './shared';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Generates robots.txt at build time. Emits `Disallow: /` when seo.noindex
|
|
@@ -13,15 +13,17 @@ export function robotsGenerator(): Plugin {
|
|
|
13
13
|
const config = readDoryConfig() || {};
|
|
14
14
|
const seo = config.seo || {};
|
|
15
15
|
|
|
16
|
+
const basePath = getBasePath(config);
|
|
16
17
|
const lines = ['User-agent: *'];
|
|
17
|
-
lines.push(seo.noindex ?
|
|
18
|
+
lines.push(seo.noindex ? `Disallow: ${basePath}/` : `Allow: ${basePath}/`);
|
|
18
19
|
|
|
19
20
|
if (Array.isArray(seo.robotsTxt) && seo.robotsTxt.length > 0) {
|
|
20
21
|
lines.push('', ...seo.robotsTxt);
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
const siteUrl = getSiteUrl(config);
|
|
25
|
+
if (siteUrl && !seo.noindex) {
|
|
26
|
+
lines.push('', `Sitemap: ${siteUrl}/sitemap.xml`);
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
this.emitFile({
|
|
@@ -60,6 +60,11 @@ describe('preprocessMdxTags', () => {
|
|
|
60
60
|
expect(run('<Note>hello</Note>')).toBe('<Note>hello</Note>');
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
+
it('preserves explicit list components', () => {
|
|
64
|
+
const input = '<UnorderedList>\n <ListItem>One</ListItem>\n</UnorderedList>\n<OrderedList><ListItem>Two</ListItem></OrderedList>';
|
|
65
|
+
expect(run(input)).toBe(input);
|
|
66
|
+
});
|
|
67
|
+
|
|
63
68
|
it('escapes known components without a closing tag', () => {
|
|
64
69
|
expect(run('<div>no closing tag')).toBe('`<div>`no closing tag');
|
|
65
70
|
});
|
package/src/plugins/sanitize.ts
CHANGED
|
@@ -24,7 +24,9 @@ const KNOWN_COMPONENTS = [
|
|
|
24
24
|
'Callout', 'Note', 'Warning', 'Info', 'Tip', 'Check',
|
|
25
25
|
'Latex', 'API', 'Expandable', 'ResponseField', 'Properties', 'Property',
|
|
26
26
|
'Steps', 'Step', 'table', 'th', 'td', 'ul', 'ol', 'li',
|
|
27
|
+
'UnorderedList', 'OrderedList', 'ListItem',
|
|
27
28
|
'APIPlayground', 'WebSocketPlayground', 'AsyncAPI', 'Source', 'Tag',
|
|
29
|
+
'Specimen', 'SpecimenProps', 'Chip', 'ChipGroup', 'Tabs', 'Tab', 'IconGrid', 'TypeScale',
|
|
28
30
|
// Common HTML elements that should be preserved
|
|
29
31
|
'div', 'span', 'br', 'hr', 'strong', 'em', 'b', 'i', 'u', 's',
|
|
30
32
|
'blockquote', 'nav', 'section', 'article', 'header', 'footer', 'main',
|
package/src/plugins/shared.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Shared helpers for build plugins that read docs/*.mdx and docs/dory.json.
|
|
@@ -33,6 +34,22 @@ export function readDoryConfig(docsDir?: string): Record<string, any> | null {
|
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
/**
|
|
38
|
+
* The base path the site is served under ("" or "/docs"), from `basePath`.
|
|
39
|
+
*/
|
|
40
|
+
export function getBasePath(config: Record<string, any> | null): string {
|
|
41
|
+
return normalizeBasePath(config?.basePath);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Absolute site URL including the base path, e.g. "https://x.dev/docs", for
|
|
46
|
+
* canonical, sitemap and llms.txt links. "" when `url` is not configured.
|
|
47
|
+
*/
|
|
48
|
+
export function getSiteUrl(config: Record<string, any> | null): string {
|
|
49
|
+
const url: string = typeof config?.url === 'string' ? config.url.trim().replace(/\/+$/, '') : '';
|
|
50
|
+
return url ? `${url}${getBasePath(config)}` : '';
|
|
51
|
+
}
|
|
52
|
+
|
|
36
53
|
/**
|
|
37
54
|
* Reads dory.json navigation and returns the flat, ordered list of page keys.
|
|
38
55
|
*/
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
pathFromFilename,
|
|
8
8
|
parseFrontmatter,
|
|
9
9
|
escapeHtml,
|
|
10
|
+
getSiteUrl,
|
|
10
11
|
} from './shared';
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -26,7 +27,7 @@ export function sitemapGenerator(): Plugin {
|
|
|
26
27
|
return;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
const baseUrl: string = config
|
|
30
|
+
const baseUrl: string = getSiteUrl(config);
|
|
30
31
|
if (!baseUrl) {
|
|
31
32
|
console.warn(
|
|
32
33
|
'⚠️ sitemap-generator: `url` is not set in dory.json — sitemap.xml will NOT be generated. Set `url` to enable it.'
|