From 999f72b76503587494158f0a149437465cad9bbd Mon Sep 17 00:00:00 2001 From: Szymon Halski Date: Thu, 20 Aug 2026 10:58:28 +0200 Subject: [PATCH 1/4] Add JSON-LD and a build-time llms.txt for the docs One local plugin injects Organization and SoftwareSourceCode structured data using the same @id as swmansion.com, and writes llms.txt from the pages Docusaurus just built. CI fails if the file is missing or lists nothing. --- .github/workflows/docs-check.yml | 14 ++ .../docs-gesture-handler/docusaurus.config.js | 1 + .../docs-gesture-handler/plugins/swm-geo.js | 135 ++++++++++++++++++ 3 files changed, 150 insertions(+) create mode 100644 packages/docs-gesture-handler/plugins/swm-geo.js diff --git a/.github/workflows/docs-check.yml b/.github/workflows/docs-check.yml index 064990d766..63d6a42726 100644 --- a/.github/workflows/docs-check.yml +++ b/.github/workflows/docs-check.yml @@ -33,3 +33,17 @@ jobs: - name: Generate docs working-directory: ${{ env.WORKING_DIRECTORY }} run: yarn build + + - name: Check docs llms.txt + run: | + file=packages/docs-gesture-handler/build/llms.txt + if [ ! -s "$file" ]; then + echo "::error::$file is missing or empty" + exit 1 + fi + count=$(grep -cE '^- \[[^]]+\]\(https://docs\.swmansion\.com/react-native-gesture-handler/[^)]+\)' "$file" || true) + if [ "$count" -eq 0 ]; then + echo "::error::$file lists no pages" + exit 1 + fi + echo "llms.txt lists $count pages" \ No newline at end of file diff --git a/packages/docs-gesture-handler/docusaurus.config.js b/packages/docs-gesture-handler/docusaurus.config.js index 13747c85e8..a5abc779f5 100644 --- a/packages/docs-gesture-handler/docusaurus.config.js +++ b/packages/docs-gesture-handler/docusaurus.config.js @@ -153,6 +153,7 @@ const config = { }, }), plugins: [ + require('./plugins/swm-geo'), ...[ process.env.NODE_ENV === 'production' && '@docusaurus/plugin-debug', process.env.NODE_ENV === 'production' && [ diff --git a/packages/docs-gesture-handler/plugins/swm-geo.js b/packages/docs-gesture-handler/plugins/swm-geo.js new file mode 100644 index 0000000000..8a96647ffc --- /dev/null +++ b/packages/docs-gesture-handler/plugins/swm-geo.js @@ -0,0 +1,135 @@ +const fs = require('node:fs'); +const path = require('node:path'); + +const ORGANIZATION_ID = 'https://swmansion.com/#organization'; +const SECTIONS = { docs: 'Documentation', blog: 'Blog', examples: 'Examples' }; + +const decode = (value) => + value + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/&#(?:39|x27);/g, "'") + .trim(); + +const metaOf = (html, name) => + new RegExp(`]+name="${name}"[^>]+content="([^"]*)"`, 'i').exec( + html, + )?.[1] ?? ''; + +function describe(html, siteTitle) { + const raw = /]*>([\s\S]*?)<\/title>/i.exec(html)?.[1] ?? ''; + const title = decode(raw).replace(new RegExp(`\\s*\\|\\s*${siteTitle}$`), ''); + return { title, description: decode(metaOf(html, 'description')) }; +} + +// Same @id as swmansion.com, so engines read one company across both domains. +function buildStructuredData(siteConfig) { + const { organizationName, projectName, tagline, title } = siteConfig; + const repository = + organizationName && projectName + ? `https://github.com/${organizationName}/${projectName}` + : undefined; + + return { + '@context': 'https://schema.org', + '@graph': [ + { + '@type': 'Organization', + '@id': ORGANIZATION_ID, + name: 'Software Mansion', + url: 'https://swmansion.com', + sameAs: [ + 'https://github.com/software-mansion', + 'https://www.linkedin.com/company/software-mansion/', + 'https://twitter.com/swmansion', + 'https://www.youtube.com/c/SoftwareMansion', + ], + }, + { + '@type': 'SoftwareSourceCode', + name: title, + ...(tagline ? { description: tagline } : {}), + ...(repository ? { codeRepository: repository } : {}), + author: { '@id': ORGANIZATION_ID }, + maintainer: { '@id': ORGANIZATION_ID }, + }, + ], + }; +} + +function buildLlmsTxt({ siteConfig, routesPaths, readPage }) { + const { baseUrl, title, tagline, url } = siteConfig; + const grouped = new Map(); + + for (const route of routesPaths) { + if (!route.startsWith(baseUrl) || route.endsWith('404.html')) continue; + + const relative = route.slice(baseUrl.length); + const html = readPage(relative); + if (!html) continue; + + const page = describe(html, title); + if (!page.title) continue; + + const section = SECTIONS[relative.split('/')[0]] ?? 'Pages'; + const line = `- [${page.title}](${url.replace(/\/$/, '')}${route})${page.description ? `: ${page.description}` : ''}`; + + if (!grouped.has(section)) grouped.set(section, []); + grouped.get(section).push(line); + } + + const lines = [`# ${title}`]; + if (tagline) lines.push('', `> ${tagline}`); + + for (const section of ['Documentation', 'Examples', 'Blog', 'Pages']) { + const entries = grouped.get(section); + if (!entries?.length) continue; + lines.push('', `## ${section}`, '', ...entries.sort()); + } + + lines.push( + '', + '## About', + '', + `- [Software Mansion](https://swmansion.com): maintainer of ${title}`, + '', + ); + + return lines.join('\n'); +} + +module.exports = function swmGeoPlugin(context) { + return { + name: 'swm-geo', + + injectHtmlTags() { + return { + headTags: [ + { + tagName: 'script', + attributes: { type: 'application/ld+json' }, + innerHTML: JSON.stringify(buildStructuredData(context.siteConfig)), + }, + ], + }; + }, + + async postBuild({ siteConfig, routesPaths, outDir }) { + const readPage = (relative) => { + const file = path.join(outDir, relative, 'index.html'); + return fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : ''; + }; + + await fs.promises.writeFile( + path.join(outDir, 'llms.txt'), + buildLlmsTxt({ siteConfig, routesPaths, readPage }), + 'utf8', + ); + }, + }; +}; + +module.exports.buildLlmsTxt = buildLlmsTxt; +module.exports.buildStructuredData = buildStructuredData; From c7150c74a9aff1696a798fcef7dd1565752fa364 Mon Sep 17 00:00:00 2001 From: Szymon Halski Date: Thu, 20 Aug 2026 11:08:16 +0200 Subject: [PATCH 2/4] Escape the site title before using it in a regex A title containing regex metacharacters would change the pattern's meaning or throw at build time. --- packages/docs-gesture-handler/plugins/swm-geo.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/docs-gesture-handler/plugins/swm-geo.js b/packages/docs-gesture-handler/plugins/swm-geo.js index 8a96647ffc..495adbd76b 100644 --- a/packages/docs-gesture-handler/plugins/swm-geo.js +++ b/packages/docs-gesture-handler/plugins/swm-geo.js @@ -13,14 +13,20 @@ const decode = (value) => .replace(/&#(?:39|x27);/g, "'") .trim(); +const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const metaOf = (html, name) => - new RegExp(`]+name="${name}"[^>]+content="([^"]*)"`, 'i').exec( - html, - )?.[1] ?? ''; + new RegExp( + `]+name="${escapeRegExp(name)}"[^>]+content="([^"]*)"`, + 'i', + ).exec(html)?.[1] ?? ''; function describe(html, siteTitle) { const raw = /]*>([\s\S]*?)<\/title>/i.exec(html)?.[1] ?? ''; - const title = decode(raw).replace(new RegExp(`\\s*\\|\\s*${siteTitle}$`), ''); + const title = decode(raw).replace( + new RegExp(`\\s*\\|\\s*${escapeRegExp(siteTitle)}$`), + '', + ); return { title, description: decode(metaOf(html, 'description')) }; } From 389e5b38dbf080f0a8684cd28b203862939fe68c Mon Sep 17 00:00:00 2001 From: Szymon Halski Date: Thu, 20 Aug 2026 15:35:41 +0200 Subject: [PATCH 3/4] Terminate the workflow file with a newline --- .github/workflows/docs-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs-check.yml b/.github/workflows/docs-check.yml index 63d6a42726..1155865161 100644 --- a/.github/workflows/docs-check.yml +++ b/.github/workflows/docs-check.yml @@ -46,4 +46,4 @@ jobs: echo "::error::$file lists no pages" exit 1 fi - echo "llms.txt lists $count pages" \ No newline at end of file + echo "llms.txt lists $count pages" From ebfa987dc26ae85d812f29d513bc420666aa3b56 Mon Sep 17 00:00:00 2001 From: Szymon Halski Date: Thu, 20 Aug 2026 15:43:39 +0200 Subject: [PATCH 4/4] Drop the page-count echo from the docs check --- .github/workflows/docs-check.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/docs-check.yml b/.github/workflows/docs-check.yml index 1155865161..160d2466e0 100644 --- a/.github/workflows/docs-check.yml +++ b/.github/workflows/docs-check.yml @@ -46,4 +46,3 @@ jobs: echo "::error::$file lists no pages" exit 1 fi - echo "llms.txt lists $count pages"