1- import fs from 'fs' ;
2- import path from 'path' ;
3-
1+ import fs from 'node:fs' ;
2+ import path from 'node:path' ;
3+ import util from 'node:util' ;
4+ import versions from '../../versions.json' ;
5+
6+ /**
7+ * Parses frontmatter from markdown content.
8+ * Extracts metadata like title and description, and returns the cleaned content.
9+ *
10+ * @param {string } fileContent - Raw markdown file content.
11+ * @returns {{data: Object, content: string} } - Parsed data and stripped content.
12+ */
413function parseFrontMatter ( fileContent ) {
514 const frontMatterRegex = / ^ - - - \n ( [ \s \S ] + ?) \n - - - \n / ;
615 const match = fileContent . match ( frontMatterRegex ) ;
@@ -13,24 +22,42 @@ function parseFrontMatter(fileContent) {
1322 const content = fileContent . replace ( frontMatterRegex , '' ) ;
1423
1524 const data = { } ;
25+
1626 frontMatterBlock . split ( '\n' ) . forEach ( ( line ) => {
1727 const parts = line . split ( ':' ) ;
28+
1829 if ( parts . length >= 2 ) {
1930 const key = parts [ 0 ] . trim ( ) ;
31+
2032 let value = parts . slice ( 1 ) . join ( ':' ) . trim ( ) ;
33+
34+ // Remove surrounding quotes if present
2135 if (
22- ( value . startsWith ( "'" ) && value . endsWith ( "'" ) ) ||
36+ ( value . startsWith ( "'" ) && value . endsWith ( "'" ) ) ||
2337 ( value . startsWith ( '"' ) && value . endsWith ( '"' ) )
2438 ) {
2539 value = value . slice ( 1 , - 1 ) ;
2640 }
41+
2742 data [ key ] = value ;
2843 }
2944 } ) ;
3045
3146 return { data, content } ;
3247}
3348
49+ /**
50+ * Recursively processes sidebar items to generate the LLM index and collect full docs.
51+ * Handles different sidebar structures (flat arrays, categories).
52+ *
53+ * @param {Array } items - Sidebar items to process.
54+ * @param {string } docsPath - Base path to documentation files.
55+ * @param {string } version - Current version being processed (e.g., '7.x').
56+ * @param {boolean } isLatest - Whether this is the latest/stable version.
57+ * @param {string } baseUrl - The base URL of the website.
58+ * @param {number } level - Current nesting level for headings.
59+ * @returns {{content: string, docs: Array} } - Generated index content and list of doc objects.
60+ */
3461function processSidebar (
3562 items ,
3663 docsPath ,
@@ -43,6 +70,7 @@ function processSidebar(
4370 let fullDocsList = [ ] ;
4471
4572 items . forEach ( ( item ) => {
73+ // Case 1: Item is a direct link (string ID)
4674 if ( typeof item === 'string' ) {
4775 const id = item ;
4876 const filePath = path . join ( docsPath , `${ id } .md` ) ;
@@ -54,13 +82,9 @@ function processSidebar(
5482 const title = data . title || id ;
5583 const description = data . description || '' ;
5684
57- let urlPath ;
58- if ( isLatest ) {
59- urlPath = `/docs/${ id } ` ;
60- } else {
61- urlPath = `/docs/${ version } /${ id } ` ;
62- }
63-
85+ // Construct the public URL for the document
86+ // Latest version uses /docs/id, others use /docs/version/id
87+ const urlPath = isLatest ? `/docs/${ id } ` : `/docs/${ version } /${ id } ` ;
6488 const fullUrl = `${ baseUrl } ${ urlPath } ` ;
6589
6690 llmsContent += `- [${ title } ](${ fullUrl } )${
@@ -73,9 +97,12 @@ function processSidebar(
7397 content,
7498 } ) ;
7599 }
76- } else if ( item . type === 'category' ) {
100+ }
101+ // Case 2: Item is a category object
102+ else if ( item . type === 'category' ) {
77103 const label = item . label ;
78- const headingPrefix = '#' . repeat ( level + 3 ) ;
104+ const headingPrefix = '#' . repeat ( level + 3 ) ; // Start at level 3 (###)
105+
79106 llmsContent += `\n${ headingPrefix } ${ label } \n\n` ;
80107
81108 const { content : childContent , docs : childDocs } = processSidebar (
@@ -86,6 +113,7 @@ function processSidebar(
86113 baseUrl ,
87114 level + 1
88115 ) ;
116+
89117 llmsContent += childContent ;
90118 fullDocsList = fullDocsList . concat ( childDocs ) ;
91119 }
@@ -94,6 +122,11 @@ function processSidebar(
94122 return { content : llmsContent , docs : fullDocsList } ;
95123}
96124
125+ /**
126+ * Generates the llms.txt and llms-full.txt files for a specific version.
127+ *
128+ * @returns {Array<string> } - List of generated filenames.
129+ */
97130function generateForVersion (
98131 siteDir ,
99132 outDir ,
@@ -102,8 +135,6 @@ function generateForVersion(
102135 isLatest ,
103136 baseUrl
104137) {
105- console . log ( `[generate-llms-txt] Generating for version ${ version } ...` ) ;
106-
107138 const docsPath = path . join ( siteDir , 'versioned_docs' , `version-${ version } ` ) ;
108139 const sidebarPath = path . join (
109140 siteDir ,
@@ -112,24 +143,27 @@ function generateForVersion(
112143 ) ;
113144
114145 if ( ! fs . existsSync ( sidebarPath ) ) {
115- console . warn (
116- `[generate-llms-txt] Sidebar not found: ${ sidebarPath } , skipping.`
117- ) ;
118- return ;
146+ // Silent return if sidebar is missing to avoid noise
147+ return [ ] ;
119148 }
120149
121150 const sidebarConfig = JSON . parse ( fs . readFileSync ( sidebarPath , 'utf8' ) ) ;
151+
152+ // Handle different Docusaurus sidebar structures (root 'docs' key or first key)
122153 let rootItems = sidebarConfig . docs || Object . values ( sidebarConfig ) [ 0 ] || [ ] ;
123154
155+ // Normalize object-style sidebars (older Docusaurus versions) to array format
124156 if ( ! Array . isArray ( rootItems ) && typeof rootItems === 'object' ) {
125157 const normalized = [ ] ;
158+
126159 for ( const [ label , items ] of Object . entries ( rootItems ) ) {
127160 normalized . push ( {
128161 type : 'category' ,
129162 label : label ,
130163 items : items ,
131164 } ) ;
132165 }
166+
133167 rootItems = normalized ;
134168 }
135169
@@ -141,12 +175,18 @@ function generateForVersion(
141175 baseUrl
142176 ) ;
143177
178+ // 1. Generate Summary (llms.txt)
144179 let llmsTxt = `# React Navigation ${ version } \n\n` ;
180+
145181 llmsTxt += `> Routing and navigation for your React Native apps.\n\n` ;
146182 llmsTxt += `## Documentation\n` ;
147-
148183 llmsTxt += sidebarContent ;
149184
185+ const summaryFilename = `${ outputPrefix } .txt` ;
186+
187+ fs . writeFileSync ( path . join ( outDir , summaryFilename ) , llmsTxt ) ;
188+
189+ // 2. Generate Full Content (llms-full.txt)
150190 let llmsFullTxt = `# React Navigation ${ version } Documentation\n\n` ;
151191
152192 docs . forEach ( ( doc ) => {
@@ -155,9 +195,9 @@ function generateForVersion(
155195 llmsFullTxt += `${ doc . content . trim ( ) } \n\n---\n\n` ;
156196 } ) ;
157197
158- const summaryFilename = `${ outputPrefix } .txt` ;
159-
198+ // Determine full filename (e.g., llms-v8.x -> llms-full-v8.x.txt)
160199 let fullFilename ;
200+
161201 if ( outputPrefix === 'llms' ) {
162202 fullFilename = 'llms-full.txt' ;
163203 } else {
@@ -168,55 +208,47 @@ function generateForVersion(
168208 }
169209 }
170210
171- fs . writeFileSync ( path . join ( outDir , summaryFilename ) , llmsTxt ) ;
172- console . log ( `[generate-llms-txt] Wrote ${ summaryFilename } ` ) ;
173-
174211 fs . writeFileSync ( path . join ( outDir , fullFilename ) , llmsFullTxt ) ;
175- console . log ( `[generate-llms-txt] Wrote ${ fullFilename } ` ) ;
212+
213+ return [ summaryFilename , fullFilename ] ;
176214}
177215
178216export default function ( context , options ) {
179217 return {
180- name : 'generate- llms- txt' ,
218+ name : 'llms. txt' ,
181219 async postBuild ( { siteDir, outDir } ) {
182220 const { latestVersion, baseUrl } = options ;
183221
184222 if ( ! latestVersion ) {
185- throw new Error (
186- '[generate-llms-txt] "latestVersion" option is required.'
187- ) ;
188- }
189- if ( ! baseUrl ) {
190- throw new Error ( '[generate-llms-txt] "baseUrl" option is required.' ) ;
223+ throw new Error ( '[llms.txt] "latestVersion" option is required.' ) ;
191224 }
192225
193- const versionsPath = path . join ( siteDir , 'versions.json' ) ;
194- let versions = [ ] ;
195- if ( fs . existsSync ( versionsPath ) ) {
196- versions = JSON . parse ( fs . readFileSync ( versionsPath , 'utf8' ) ) ;
197- } else {
198- console . warn (
199- '[generate-llms-txt] versions.json not found. Generating only for latestVersion.'
200- ) ;
201- versions = [ latestVersion ] ;
226+ if ( ! baseUrl ) {
227+ throw new Error ( '[llms.txt] "baseUrl" option is required.' ) ;
202228 }
203229
204- console . log ( `[generate-llms-txt] Found versions: ${ versions . join ( ', ' ) } ` ) ;
205- console . log ( `[generate-llms-txt] Latest version: ${ latestVersion } ` ) ;
230+ const generatedFiles = [ ] ;
206231
207232 versions . forEach ( ( version ) => {
208233 const isLatest = version === latestVersion ;
234+ // Prefix: 'llms' for latest, 'llms-vX.x' for others
209235 const outputPrefix = isLatest ? 'llms' : `llms-v${ version } ` ;
210236
211- generateForVersion (
237+ const files = generateForVersion (
212238 siteDir ,
213239 outDir ,
214240 version ,
215241 outputPrefix ,
216242 isLatest ,
217243 baseUrl
218244 ) ;
245+
246+ generatedFiles . push ( ...files ) ;
219247 } ) ;
248+
249+ console . log (
250+ `${ util . styleText ( [ 'magenta' , 'bold' ] , '[llms.txt]' ) } Wrote ${ generatedFiles . length } files in build output.`
251+ ) ;
220252 } ,
221253 } ;
222254}
0 commit comments