1- import { promises as fs } from 'node:fs ' ;
2- import { join } from 'node:path ' ;
1+ import type { CollectionEntry } from 'astro:content ' ;
2+ import { getCollection } from 'astro:content ' ;
33import type { APIRoute } from 'astro' ;
4+ import { sidebarConfig } from '@/lib/sidebar' ;
5+ import { cleanMdxContent } from '@/lib/utils' ;
6+
7+ import type { APIRoute } from 'astro' ;
8+ import type { CollectionEntry } from 'astro:content' ;
49
510export const prerender = true ;
611
7- // Use process.cwd() which is the project root during Astro build
8- const docsDir = join ( process . cwd ( ) , 'src/content/docs' ) ;
9-
10- // Document order matching the sidebar structure
11- const docOrder = [
12- // Getting Started
13- 'index.mdx' ,
14- 'quickstart.mdx' ,
15- 'working-with-sprites.mdx' ,
16- // Concepts
17- 'concepts/lifecycle.mdx' ,
18- 'concepts/services.mdx' ,
19- 'concepts/networking.mdx' ,
20- 'concepts/checkpoints.mdx' ,
21- // CLI
22- 'cli/installation.mdx' ,
23- 'cli/authentication.mdx' ,
24- 'cli/commands.mdx' ,
25- // SDKs
26- 'sdks/javascript.mdx' ,
27- 'sdks/go.mdx' ,
28- // API (generated)
29- 'api/index.mdx' ,
30- 'api/exec.mdx' ,
31- 'api/checkpoints.mdx' ,
32- 'api/services.mdx' ,
33- 'api/proxy.mdx' ,
34- 'api/policy.mdx' ,
35- 'api/types.mdx' ,
36- // Reference
37- 'reference/base-images.mdx' ,
38- 'reference/configuration.mdx' ,
39- 'reference/billing.mdx' ,
40- ] ;
41-
42- // Section headers for organization
43- const sections : Record < string , string > = {
44- 'index.mdx' : '# Getting Started' ,
45- 'concepts/lifecycle.mdx' : '# Concepts' ,
46- 'cli/installation.mdx' : '# CLI' ,
47- 'sdks/javascript.mdx' : '# SDKs' ,
48- 'api/index.mdx' : '# API' ,
49- 'reference/base-images.mdx' : '# Reference' ,
12+ export type DocsGroup = {
13+ label : string ;
14+ items : {
15+ slug : string ;
16+ body : string ;
17+ title : string ;
18+ description ?: string ;
19+ } [ ] ;
5020} ;
5121
52- interface DocMeta {
53- title : string ;
54- description ?: string ;
55- }
56-
57- function extractFrontmatter ( content : string ) : { meta : DocMeta ; body : string } {
58- const frontmatterMatch = content . match ( / ^ - - - \n ( [ \s \S ] * ?) \n - - - \n ( [ \s \S ] * ) $ / ) ;
59-
60- if ( ! frontmatterMatch ) {
61- return { meta : { title : 'Untitled' } , body : content } ;
62- }
63-
64- const [ , frontmatterStr , body ] = frontmatterMatch ;
65- const meta : DocMeta = { title : 'Untitled' } ;
22+ export async function getGroupedDocs ( ) : Promise < DocsGroup [ ] > {
23+ const collection = await getCollection ( 'docs' , ( { data } ) => {
24+ return data . draft !== true ;
25+ } ) ;
6626
67- // Parse YAML-like frontmatter (simple key: value parsing)
68- for ( const line of frontmatterStr . split ( '\n' ) ) {
69- const titleMatch = line . match ( / ^ t i t l e : \s * ( .+ ) $ / ) ;
70- if ( titleMatch ) {
71- meta . title = titleMatch [ 1 ] . replace ( / ^ [ " ' ] | [ " ' ] $ / g, '' ) ;
72- }
73- const descMatch = line . match ( / ^ d e s c r i p t i o n : \s * ( .+ ) $ / ) ;
74- if ( descMatch ) {
75- meta . description = descMatch [ 1 ] . replace ( / ^ [ " ' ] | [ " ' ] $ / g, '' ) ;
76- }
27+ const atlas = new Map < string , CollectionEntry < 'docs' > > ( ) ;
28+ for ( const doc of collection ) {
29+ atlas . set ( doc . id , doc ) ;
7730 }
7831
79- return { meta, body } ;
80- }
81-
82- function cleanMdxContent ( content : string ) : string {
83- // Remove MDX import statements at the start of the file (before any content)
84- // This preserves imports inside code blocks
85- content = content . replace (
86- / ^ ( \s * i m p o r t \s + .* ?(?: f r o m \s + [ ' " ] .* ?[ ' " ] ) ? ; ? \s * \n ) + / m,
87- '' ,
88- ) ;
89-
90- // Process Tabs components - extract TabItem contents
91- // Need to handle nested content carefully (code blocks with special chars)
92- content = content . replace ( / < T a b s > [ \s \S ] * ?< \/ T a b s > / g, ( match ) => {
93- const results : string [ ] = [ ] ;
94-
95- // Split by TabItem boundaries and extract content
96- const tabItemRegex =
97- / < T a b I t e m [ ^ > ] * l a b e l = " ( [ ^ " ] * ) " [ ^ > ] * > ( [ \s \S ] * ?) (? = < T a b I t e m | < \/ T a b s > ) / g;
98-
99- for ( const [ , label , tabContent ] of match . matchAll ( tabItemRegex ) ) {
100- // Clean up the content - remove closing </TabItem> if present
101- const cleanContent = tabContent . replace ( / < \/ T a b I t e m > \s * $ / , '' ) . trim ( ) ;
102- if ( cleanContent ) {
103- results . push ( `**${ label } :**\n${ cleanContent } ` ) ;
32+ const groups = [ ] ;
33+ for ( const { label, items : sidebarItems } of sidebarConfig ) {
34+ const items = [ ] ;
35+ for ( const sidebarItem of sidebarItems ) {
36+ if (
37+ typeof sidebarItem === 'object' &&
38+ sidebarItem != null &&
39+ 'slug' in sidebarItem
40+ ) {
41+ const doc = atlas . get ( sidebarItem . slug ) ;
42+ if ( doc != null && doc . body != null ) {
43+ items . push ( {
44+ slug : doc . id ,
45+ body : doc . body ,
46+ title : doc . data . title ,
47+ description : doc . data . description ,
48+ } ) ;
49+ } else {
50+ console . warn ( `Warning: Could not find ${ sidebarItem . label } :` ) ;
51+ }
10452 }
10553 }
54+ groups . push ( { label, items } ) ;
55+ }
10656
107- return results . length > 0 ? results . join ( '\n\n' ) : '' ;
108- } ) ;
109-
110- // Remove self-closing JSX/MDX components (like <Callout ... />)
111- content = content . replace ( / < [ A - Z ] [ a - z A - Z ] * \s + [ ^ > ] * \/ > / g, '' ) ;
112-
113- // Remove JSX components with content (non-greedy, for simple components)
114- // Handle Callout, Snippet, and other simple wrapper components
115- content = content . replace (
116- / < C a l l o u t [ ^ > ] * > ( [ \s \S ] * ?) < \/ C a l l o u t > / g,
117- ( _ , inner ) => {
118- // Keep the content, just remove the wrapper
119- return inner . trim ( ) ;
120- } ,
121- ) ;
122-
123- // Remove remaining JSX component tags (opening and closing)
124- content = content . replace ( / < [ A - Z ] [ a - z A - Z ] * [ ^ > ] * > / g, '' ) ;
125- content = content . replace ( / < \/ [ A - Z ] [ a - z A - Z ] * > / g, '' ) ;
126-
127- // Convert relative links to fully qualified URLs
128- // Matches markdown links like [text](/path) or [text](/path/)
129- content = content . replace (
130- / \[ ( [ ^ \] ] + ) \] \( \/ ( [ ^ ) ] * ) \) / g,
131- ( _ , text , path ) => `[${ text } ](https://docs.sprites.dev/${ path } )` ,
132- ) ;
133-
134- // Clean up excessive blank lines
135- content = content . replace ( / \n { 4 , } / g, '\n\n\n' ) ;
136-
137- // Trim leading/trailing whitespace
138- content = content . trim ( ) ;
139-
140- return content ;
141- }
142-
143- function slugToUrl ( slug : string ) : string {
144- const path = slug . replace ( / \. m d x $ / , '' ) . replace ( / ^ i n d e x $ / , '' ) ;
145- return `https://docs.sprites.dev/${ path } ${ path ? '/' : '' } ` ;
57+ return groups ;
14658}
14759
14860export const GET : APIRoute = async ( ) => {
@@ -160,34 +72,21 @@ Summary: https://docs.sprites.dev/llms.txt
16072---
16173` ) ;
16274
163- let currentSection = '' ;
164-
165- for ( const docPath of docOrder ) {
166- const fullPath = join ( docsDir , docPath ) ;
167-
168- try {
169- const content = await fs . readFile ( fullPath , 'utf-8' ) ;
170- const { meta, body } = extractFrontmatter ( content ) ;
75+ const groups = await getGroupedDocs ( ) ;
76+ for ( const { label, items } of groups ) {
77+ parts . push ( `\n# ${ label } \n` ) ;
78+ for ( const { slug, title, description, body } of items ) {
17179 const cleanedContent = cleanMdxContent ( body ) ;
17280
173- // Add section header if we're entering a new section
174- if ( sections [ docPath ] && sections [ docPath ] !== currentSection ) {
175- currentSection = sections [ docPath ] ;
176- parts . push ( `\n${ currentSection } \n` ) ;
177- }
178-
17981 // Add document with title and URL
180- const url = slugToUrl ( docPath ) ;
181- parts . push ( `## ${ meta . title }
82+ parts . push ( `## ${ title }
18283
183- URL: ${ url }
184- ${ meta . description ? `\n${ meta . description } \n` : '' }
84+ URL: https://docs.sprites.dev/ ${ slug } .md
85+ ${ description ? `\n${ description } \n` : '' }
18586${ cleanedContent }
18687
18788---
18889` ) ;
189- } catch ( error ) {
190- console . warn ( `Warning: Could not read ${ docPath } :` , error ) ;
19190 }
19291 }
19392
0 commit comments