Skip to content

Commit 1c7fc79

Browse files
etrepumclaude
andauthored
[lexical-website] Feature: Server-rendered "Copy page" Markdown button (#8570)
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8f19295 commit 1c7fc79

10 files changed

Lines changed: 752 additions & 1 deletion

File tree

packages/lexical-website/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
.cache-loader
1010
/docs/api
1111
/docs/packages
12+
/static/llms
1213

1314
# Misc
1415
.DS_Store

packages/lexical-website/docusaurus.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {fileURLToPath} from 'node:url';
1717
import {themes} from 'prism-react-renderer';
1818

1919
import {packagesManager} from '../../scripts/shared/packagesManager.mjs';
20+
import copyPageButtonPlugin from './plugins/copy-page-button/index.mjs';
2021
import packageDocsPlugin from './plugins/package-docs/index.mjs';
2122
import slugifyPlugin from './src/plugins/lexical-remark-slugify-anchors/index.js';
2223

@@ -345,6 +346,7 @@ const config: Config = {
345346
},
346347
],
347348
'./plugins/webpack-buffer',
349+
copyPageButtonPlugin,
348350
async function webpackLexicalModules() {
349351
return {
350352
configureWebpack(_config, _isServer, {currentBundler}) {

packages/lexical-website/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"@docusaurus/core": "^3.10.1",
1919
"@docusaurus/faster": "^3.10.1",
2020
"@docusaurus/plugin-client-redirects": "^3.10.1",
21+
"@docusaurus/plugin-content-docs": "^3.10.1",
2122
"@docusaurus/preset-classic": "^3.10.1",
2223
"@docusaurus/theme-common": "^3.10.1",
2324
"@docusaurus/theme-mermaid": "^3.10.1",
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
*/
8+
9+
import fs from 'node:fs';
10+
import path from 'node:path';
11+
12+
import {MARKDOWN_NAMESPACE, relativeMarkdownPath} from './markdownPath.mjs';
13+
14+
const SITE_ALIAS = '@site';
15+
16+
function stripFrontMatter(raw) {
17+
return raw.replace(/^\uFEFF?---\r?\n[\s\S]*?\r?\n---\r?\n?/, '');
18+
}
19+
20+
/**
21+
* Drop the leading block of MDX `import`/`export` statements (and blank lines)
22+
* that appear before the first piece of real content. Only the leading block is
23+
* removed so `import`/`export` lines inside code fences are left untouched.
24+
*/
25+
function stripLeadingMdxStatements(body) {
26+
const lines = body.split('\n');
27+
let index = 0;
28+
for (; index < lines.length; index++) {
29+
const trimmed = lines[index].trim();
30+
if (trimmed === '' || /^(?:import|export)\b/.test(trimmed)) {
31+
continue;
32+
}
33+
break;
34+
}
35+
return lines.slice(index).join('\n');
36+
}
37+
38+
/**
39+
* Emit a clean Markdown copy of every doc page at build time so the
40+
* server-rendered CopyPageButton can link to / copy / hand off real Markdown
41+
* without any client-side DOM scraping.
42+
*
43+
* @type {import('@docusaurus/types').PluginModule}
44+
*/
45+
const copyPageButtonPlugin = async function (context) {
46+
const {siteDir, siteConfig} = context;
47+
const {baseUrl, url: siteUrl} = siteConfig;
48+
const outputRoot = path.join(siteDir, 'static', MARKDOWN_NAMESPACE);
49+
50+
const resolveSource = source => {
51+
if (source.startsWith(SITE_ALIAS)) {
52+
return path.join(siteDir, source.slice(SITE_ALIAS.length));
53+
}
54+
return path.isAbsolute(source) ? source : path.join(siteDir, source);
55+
};
56+
57+
return {
58+
// Runs in both dev and production, after every plugin has loaded its
59+
// content, so we have the authoritative permalink -> source mapping for
60+
// every doc (including the generated API reference).
61+
allContentLoaded({allContent}) {
62+
const docsContent = allContent['docusaurus-plugin-content-docs'];
63+
if (!docsContent) {
64+
return;
65+
}
66+
67+
// Regenerate from scratch so renamed/removed pages don't leave orphans.
68+
fs.rmSync(outputRoot, {force: true, recursive: true});
69+
70+
const normalizedSiteUrl = String(siteUrl || '').replace(/\/$/, '');
71+
72+
for (const instance of Object.values(docsContent)) {
73+
const loadedVersions = (instance && instance.loadedVersions) || [];
74+
for (const version of loadedVersions) {
75+
for (const doc of version.docs || []) {
76+
const sourcePath = resolveSource(doc.source);
77+
let raw;
78+
try {
79+
raw = fs.readFileSync(sourcePath, 'utf-8');
80+
} catch {
81+
continue;
82+
}
83+
84+
let body = stripFrontMatter(raw);
85+
if (sourcePath.endsWith('.mdx')) {
86+
body = stripLeadingMdxStatements(body);
87+
}
88+
body = body.trim();
89+
90+
const pageUrl = `${normalizedSiteUrl}${doc.permalink}`;
91+
const header = /^#\s/.test(body)
92+
? `URL: ${pageUrl}\n\n`
93+
: `# ${doc.title}\n\nURL: ${pageUrl}\n\n`;
94+
95+
const outputPath = path.join(
96+
outputRoot,
97+
`${relativeMarkdownPath(doc.permalink, baseUrl)}.md`,
98+
);
99+
fs.mkdirSync(path.dirname(outputPath), {recursive: true});
100+
fs.writeFileSync(outputPath, `${header}${body}\n`);
101+
}
102+
}
103+
}
104+
},
105+
106+
name: 'copy-page-button',
107+
};
108+
};
109+
110+
export default copyPageButtonPlugin;
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
*/
8+
9+
/**
10+
* Path namespace (under `static/`) where the build-time plugin emits a Markdown
11+
* copy of every doc page, e.g. the page `/docs/intro` is served at
12+
* `/llms/docs/intro.md`.
13+
*/
14+
export const MARKDOWN_NAMESPACE = 'llms';
15+
16+
/**
17+
* Map a doc permalink to the namespace-relative path of its generated Markdown
18+
* file (without the `MARKDOWN_NAMESPACE` prefix or surrounding slashes), e.g.
19+
* `/docs/api/` -> `docs/api`.
20+
*
21+
* Shared by the plugin that writes the file and the CopyPageButton that links
22+
* to it so the two normalizations (notably trailing-slash handling for index
23+
* pages) can never drift.
24+
*
25+
* @param {string} permalink Doc permalink, including baseUrl (e.g. `/docs/api/`).
26+
* @param {string} baseUrl Site baseUrl (e.g. `/`).
27+
* @returns {string}
28+
*/
29+
export function relativeMarkdownPath(permalink, baseUrl) {
30+
let rel = permalink;
31+
if (baseUrl && rel.startsWith(baseUrl)) {
32+
rel = rel.slice(baseUrl.length);
33+
}
34+
return rel.replace(/^\/+/, '').replace(/\/+$/, '') || 'index';
35+
}

0 commit comments

Comments
 (0)