diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index a60c311a2..1ba811cb5 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -2,6 +2,20 @@ const webpack = require('webpack'); const {getDocusaurusConfig} = require('@vis.gl/docusaurus-website'); const {resolve} = require('path'); +const SITE_URL = 'https://visgl.github.io/react-map-gl'; + +const sharedDocPatterns = [ + 'README.md', + 'whats-new.md', + 'upgrade-guide.md', + 'contributing.md', + 'get-started/**' +]; +const mapboxApiPatterns = ['api-reference/mapbox/**']; +const maplibreApiPatterns = ['api-reference/maplibre/**']; +const mapboxDocPatterns = [...sharedDocPatterns, ...mapboxApiPatterns]; +const maplibreDocPatterns = [...sharedDocPatterns, ...maplibreApiPatterns]; + const config = getDocusaurusConfig({ projectName: 'react-map-gl', tagline: 'React components for Mapbox GL JS and Maplibre GL JS', @@ -39,6 +53,75 @@ const config = getDocusaurusConfig({ './src/styles.css', './src/mapbox-gl.css', './src/maplibre-gl.css' + ], + + plugins: [ + [ + 'docusaurus-plugin-llms', + { + docsDir: [{path: '../docs', routeBasePath: 'docs', label: 'Docs'}], + generateLLMsTxt: false, + generateLLMsFullTxt: false, + generateMarkdownFiles: false, + excludeImports: true, + removeDuplicateHeadings: true, + title: 'react-map-gl', + description: 'React components for Mapbox GL JS and MapLibre GL JS', + customLLMFiles: [ + { + filename: 'llms.txt', + fullContent: false, + title: 'react-map-gl', + description: 'React components for Mapbox GL JS and MapLibre GL JS', + includePatterns: sharedDocPatterns, + orderPatterns: sharedDocPatterns, + includeUnmatchedLast: false, + rootContent: `Choose the documentation bundle for your base map library: + +- [Mapbox GL JS index](${SITE_URL}/llms-mapbox.txt) (full content: [llms-mapbox-full.txt](${SITE_URL}/llms-mapbox-full.txt)) +- [MapLibre GL JS index](${SITE_URL}/llms-maplibre.txt) (full content: [llms-maplibre-full.txt](${SITE_URL}/llms-maplibre-full.txt)) + +Only load one stack's files — API reference pages are parallel but not interchangeable.` + }, + { + filename: 'llms-mapbox.txt', + fullContent: false, + title: 'react-map-gl (Mapbox GL JS)', + description: 'Docs for react-map-gl with Mapbox GL JS', + includePatterns: mapboxDocPatterns, + orderPatterns: mapboxDocPatterns, + includeUnmatchedLast: false + }, + { + filename: 'llms-mapbox-full.txt', + fullContent: true, + title: 'react-map-gl (Mapbox GL JS) — full', + description: 'Docs for react-map-gl with Mapbox GL JS', + includePatterns: mapboxDocPatterns, + orderPatterns: mapboxDocPatterns, + includeUnmatchedLast: false + }, + { + filename: 'llms-maplibre.txt', + fullContent: false, + title: 'react-map-gl (MapLibre GL JS)', + description: 'Docs for react-map-gl with MapLibre GL JS', + includePatterns: maplibreDocPatterns, + orderPatterns: maplibreDocPatterns, + includeUnmatchedLast: false + }, + { + filename: 'llms-maplibre-full.txt', + fullContent: true, + title: 'react-map-gl (MapLibre GL JS) — full', + description: 'Docs for react-map-gl with MapLibre GL JS', + includePatterns: maplibreDocPatterns, + orderPatterns: maplibreDocPatterns, + includeUnmatchedLast: false + } + ] + } + ] ] }); diff --git a/website/package.json b/website/package.json index a09db370a..acbb73f21 100644 --- a/website/package.json +++ b/website/package.json @@ -5,7 +5,7 @@ "description": "Website for vis.gl project", "scripts": { "start": "docusaurus start", - "build": "docusaurus build", + "build": "docusaurus build && node scripts/append-examples-to-llms.js", "clear": "docusaurus clear", "serve": "docusaurus serve", "write-heading-ids": "ocular-doc-headers ../docs", @@ -22,9 +22,9 @@ "@mapbox/mapbox-gl-draw": "^1.3.0", "@mapbox/mapbox-gl-geocoder": "^4.7.4", "@maplibre/maplibre-gl-geocoder": "^1.5.0", - "@vis.gl/docusaurus-website": "1.0.0-alpha.23", "@turf/area": "^6.5.0", "@turf/bbox": "^6.5.0", + "@vis.gl/docusaurus-website": "1.0.0-alpha.23", "d3-array": "^3.1.1", "d3-scale": "^4.0.2", "immutable": "^4.0.0", @@ -40,5 +40,8 @@ "volta": { "node": "18.20.5", "yarn": "1.22.22" + }, + "devDependencies": { + "docusaurus-plugin-llms": "^0.4.0" } } diff --git a/website/scripts/append-examples-to-llms.js b/website/scripts/append-examples-to-llms.js new file mode 100644 index 000000000..99ed67368 --- /dev/null +++ b/website/scripts/append-examples-to-llms.js @@ -0,0 +1,177 @@ +const fs = require('fs'); +const path = require('path'); + +const SITE_URL = 'https://visgl.github.io/react-map-gl'; +const BUILD_DIR = path.join(__dirname, '../build'); +const REPO_ROOT = path.join(__dirname, '../..'); +const EXAMPLES_TOC = path.join(__dirname, '../src/examples/table-of-contents.json'); + +function getExampleIdsByStack(toc) { + const stacks = {mapbox: [], maplibre: []}; + + for (const entry of toc) { + if (entry.type !== 'category') { + continue; + } + const label = entry.label.toLowerCase(); + if (label === 'mapbox') { + stacks.mapbox = entry.items; + } else if (label === 'maplibre') { + stacks.maplibre = entry.items; + } + } + + return stacks; +} + +function readTitle(exampleId) { + const mdxPath = path.join(__dirname, '../src/examples', `${exampleId}.mdx`); + if (fs.existsSync(mdxPath)) { + const firstLine = fs.readFileSync(mdxPath, 'utf8').split('\n')[0]; + const match = firstLine.match(/^#\s+(.+)$/); + if (match) { + return match[1].trim(); + } + } + + const readmePath = path.join(REPO_ROOT, 'examples', exampleId, 'README.md'); + if (fs.existsSync(readmePath)) { + const firstLine = fs.readFileSync(readmePath, 'utf8').split('\n')[0]; + const match = firstLine.match(/^#\s+Example:\s*(.+)$/); + if (match) { + return match[1].trim(); + } + } + + const slug = exampleId.split('/').pop(); + return slug + .split('-') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); +} + +function readBlurb(exampleId) { + const readmePath = path.join(REPO_ROOT, 'examples', exampleId, 'README.md'); + if (!fs.existsSync(readmePath)) { + return null; + } + + const content = fs.readFileSync(readmePath, 'utf8'); + const showcaseMatch = content.match(/This example showcases how to ([^\n.]+(?:\.[^\n.]+)*)\./i); + if (showcaseMatch) { + return showcaseMatch[1].trim().replace(/\.$/, ''); + } + + return null; +} + +function readReadmeIntro(exampleId) { + const readmePath = path.join(REPO_ROOT, 'examples', exampleId, 'README.md'); + if (!fs.existsSync(readmePath)) { + return null; + } + + const lines = fs.readFileSync(readmePath, 'utf8').split('\n'); + const intro = []; + + for (let i = 1; i < lines.length; i++) { + const line = lines[i].trim(); + if (line.startsWith('## Usage')) { + break; + } + if (line) { + intro.push(line); + } + } + + return intro.join('\n').trim() || null; +} + +function buildExampleLinks(exampleIds) { + return exampleIds.map(exampleId => { + const title = readTitle(exampleId); + const blurb = readBlurb(exampleId); + const url = `${SITE_URL}/examples/${exampleId}`; + const description = blurb ? `: ${blurb}` : ''; + return `- [${title}](${url})${description}`; + }); +} + +function buildExampleSections(exampleIds) { + const sections = []; + + for (const exampleId of exampleIds) { + const title = readTitle(exampleId); + const url = `${SITE_URL}/examples/${exampleId}`; + const intro = readReadmeIntro(exampleId); + const blurb = readBlurb(exampleId); + + sections.push(`### ${title}\n\n[View example](${url})`); + if (intro) { + sections.push(`\n${intro}`); + } else if (blurb) { + sections.push(`\nThis example showcases how to ${blurb}.`); + } + sections.push(''); + } + + return sections.join('\n'); +} + +function appendSection(filePath, section) { + if (!fs.existsSync(filePath)) { + console.warn(`Skipping ${path.basename(filePath)} — file not found`); + return; + } + + const content = fs.readFileSync(filePath, 'utf8').trimEnd(); + fs.writeFileSync(filePath, `${content}\n\n${section}\n`); +} + +function fixDocUrls(content) { + return content + .replace( + /https:\/\/visgl\.github\.io\/docs\/\.\.\/docs\/README\.md/g, + `${SITE_URL}/docs` + ) + .replace( + /https:\/\/visgl\.github\.io\/react-map-gl\/docs\/get-started\/get-started\.md/g, + `${SITE_URL}/docs/get-started.md` + ); +} + +function fixUrlsInLlmsFiles() { + const llmsFiles = fs + .readdirSync(BUILD_DIR) + .filter(name => name.startsWith('llms') && name.endsWith('.txt')) + .map(name => path.join(BUILD_DIR, name)); + + for (const filePath of llmsFiles) { + const fixed = fixDocUrls(fs.readFileSync(filePath, 'utf8')); + fs.writeFileSync(filePath, fixed); + } +} + +function main() { + const toc = JSON.parse(fs.readFileSync(EXAMPLES_TOC, 'utf8')); + const stacks = getExampleIdsByStack(toc); + + for (const [stack, exampleIds] of Object.entries(stacks)) { + if (exampleIds.length === 0) { + continue; + } + + const linksSection = `## Examples\n\n${buildExampleLinks(exampleIds).join('\n')}`; + appendSection(path.join(BUILD_DIR, `llms-${stack}.txt`), linksSection); + + const fullSection = `## Examples\n\n${buildExampleSections(exampleIds)}`; + appendSection(path.join(BUILD_DIR, `llms-${stack}-full.txt`), fullSection); + + console.log(`Appended ${exampleIds.length} ${stack} examples to llms-${stack}.txt`); + } + + fixUrlsInLlmsFiles(); + console.log('Fixed doc URLs in generated llms files'); +} + +main(); diff --git a/website/yarn.lock b/website/yarn.lock index 4d7be9161..b13ee56bf 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -3483,6 +3483,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.2: + version "2.1.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.1.1.tgz#c68b1c4111c76aae3a6fba55d496cee10c39dad8" + integrity sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA== + dependencies: + balanced-match "^1.0.0" + braces@^3.0.3, braces@~3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" @@ -4438,6 +4445,15 @@ dns-packet@^5.2.2: dependencies: "@leichtgewicht/ip-codec" "^2.0.1" +docusaurus-plugin-llms@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/docusaurus-plugin-llms/-/docusaurus-plugin-llms-0.4.0.tgz#020f00d83459c7feb71c7b6e61774d864336d8ae" + integrity sha512-jYlj2HJ5+gu7oJZuJ83Hk8KlB65YlZZ/7UpHXiL7Qr+qpNBkVocmt2Molc6F3HNr5RqcfhWD/98CvgyNztg/ow== + dependencies: + gray-matter "^4.0.3" + minimatch "^9.0.3" + yaml "^2.8.1" + dom-converter@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" @@ -7192,6 +7208,13 @@ minimatch@3.1.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1: dependencies: brace-expansion "^1.1.7" +minimatch@^9.0.3: + version "9.0.9" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.9.tgz#9b0cb9fcb78087f6fd7eababe2511c4d3d60574e" + integrity sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg== + dependencies: + brace-expansion "^2.0.2" + minimist-options@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" @@ -10205,6 +10228,11 @@ yaml@^1.7.2: resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== +yaml@^2.8.1: + version "2.9.0" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.9.0.tgz#78274afd93598a1dfdd6130df6a566defcbf9aa4" + integrity sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA== + yargs-parser@^20.2.3: version "20.2.9" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"