Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(llms): implement LLMs.txt support and related routes #3407

Merged
merged 2 commits into from
Mar 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions website/src/app/(llms)/llms-full.txt/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { cleanupPageContent } from '~/app/(llms)/shared'
import { frameworks } from '~/app/sitemap'
import { categories, getSidebarGroups } from '~/lib/sidebar'
import type { Pages } from '.velite'

export const dynamic = 'force-static'

const generatePageContent = async (page: Pages) =>
(
await Promise.all(
frameworks.map(async (framework) => {
return `# ${page.title} (${framework.toUpperCase()})\n\n${await cleanupPageContent(page, framework)}\n\n`
}),
)
).join('\n')

const generateCategorySection = async (category: string, pages: Pages[]) => {
const header = `# ${category.toUpperCase()}\n\n---\n`
const pagesContent = await Promise.all(pages.map(generatePageContent))
return `${header}\n${pagesContent.join('\n')}`
}

export const GET = async () => {
const sidebarGroups = getSidebarGroups()
const content = await Promise.all(
categories.map((category, index) => generateCategorySection(category, sidebarGroups[index])),
).then((sections) => sections.join('\n\n'))

return new Response(content)
}
23 changes: 23 additions & 0 deletions website/src/app/(llms)/llms-react.txt/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { cleanupPageContent } from '~/app/(llms)/shared'
import { frameworks } from '~/app/sitemap'
import { categories, getSidebarGroups } from '~/lib/sidebar'
import type { Pages } from '.velite'

export const dynamic = 'force-static'

const generatePageContent = async (page: Pages) => `# ${page.title}\n\n${await cleanupPageContent(page, 'react')}\n\n`

const generateCategorySection = async (category: string, pages: Pages[]) => {
const header = `# ${category.toUpperCase()}\n\n---\n`
const pagesContent = await Promise.all(pages.map(generatePageContent))
return `${header}\n${pagesContent.join('\n')}`
}

export const GET = async () => {
const sidebarGroups = getSidebarGroups()
const content = await Promise.all(
categories.map((category, index) => generateCategorySection(category, sidebarGroups[index])),
).then((sections) => sections.join('\n\n'))

return new Response(content)
}
23 changes: 23 additions & 0 deletions website/src/app/(llms)/llms-solid.txt/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { cleanupPageContent } from '~/app/(llms)/shared'
import { frameworks } from '~/app/sitemap'
import { categories, getSidebarGroups } from '~/lib/sidebar'
import type { Pages } from '.velite'

export const dynamic = 'force-static'

const generatePageContent = async (page: Pages) => `# ${page.title}\n\n${await cleanupPageContent(page, 'solid')}\n\n`

const generateCategorySection = async (category: string, pages: Pages[]) => {
const header = `# ${category.toUpperCase()}\n\n---\n`
const pagesContent = await Promise.all(pages.map(generatePageContent))
return `${header}\n${pagesContent.join('\n')}`
}

export const GET = async () => {
const sidebarGroups = getSidebarGroups()
const content = await Promise.all(
categories.map((category, index) => generateCategorySection(category, sidebarGroups[index])),
).then((sections) => sections.join('\n\n'))

return new Response(content)
}
23 changes: 23 additions & 0 deletions website/src/app/(llms)/llms-svelte.txt/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { cleanupPageContent } from '~/app/(llms)/shared'
import { frameworks } from '~/app/sitemap'
import { categories, getSidebarGroups } from '~/lib/sidebar'
import type { Pages } from '.velite'

export const dynamic = 'force-static'

const generatePageContent = async (page: Pages) => `# ${page.title}\n\n${await cleanupPageContent(page, 'svelte')}\n\n`

const generateCategorySection = async (category: string, pages: Pages[]) => {
const header = `# ${category.toUpperCase()}\n\n---\n`
const pagesContent = await Promise.all(pages.map(generatePageContent))
return `${header}\n${pagesContent.join('\n')}`
}

export const GET = async () => {
const sidebarGroups = getSidebarGroups()
const content = await Promise.all(
categories.map((category, index) => generateCategorySection(category, sidebarGroups[index])),
).then((sections) => sections.join('\n\n'))

return new Response(content)
}
23 changes: 23 additions & 0 deletions website/src/app/(llms)/llms-vue.txt/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { cleanupPageContent } from '~/app/(llms)/shared'
import { frameworks } from '~/app/sitemap'
import { categories, getSidebarGroups } from '~/lib/sidebar'
import type { Pages } from '.velite'

export const dynamic = 'force-static'

const generatePageContent = async (page: Pages) => `# ${page.title}\n\n${await cleanupPageContent(page, 'vue')}\n\n`

const generateCategorySection = async (category: string, pages: Pages[]) => {
const header = `# ${category.toUpperCase()}\n\n---\n`
const pagesContent = await Promise.all(pages.map(generatePageContent))
return `${header}\n${pagesContent.join('\n')}`
}

export const GET = async () => {
const sidebarGroups = getSidebarGroups()
const content = await Promise.all(
categories.map((category, index) => generateCategorySection(category, sidebarGroups[index])),
).then((sections) => sections.join('\n\n'))

return new Response(content)
}
25 changes: 25 additions & 0 deletions website/src/app/(llms)/llms.txt/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { frameworks } from '~/app/sitemap'
import { categories, getSidebarGroups } from '~/lib/sidebar'

export const dynamic = 'force-static'

export const GET = async () => {
const sidebarGroups = getSidebarGroups()

const generateUrl = (framework: string, slug: string) => `https://ark-ui.com/${framework}/docs/${slug}`

const generatePageLinks = (page: { title: string; slug: string }) =>
frameworks.map((framework) => `- [${page.title}](${generateUrl(framework, page.slug)})`).join('\n')

const generateCategorySection = (category: string, pages: (typeof sidebarGroups)[number]) => {
const header = `# ${category.toUpperCase()}\n`
const pageLinks = pages.map(generatePageLinks).join('\n')
return `${header}\n${pageLinks}`
}

const content = categories
.map((category, index) => generateCategorySection(category, sidebarGroups[index]))
.join('\n\n')

return new Response(content)
}
150 changes: 150 additions & 0 deletions website/src/app/(llms)/shared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { type AccessibilityDocKey, type DataAttrDocKey, getAccessibilityDoc, getDataAttrDoc } from '@zag-js/docs'
import { frameworkExample } from '~/components/example'
import { cmdMap } from '~/components/install-cmd'
import type { Pages } from '.velite'
import { types } from '.velite'

// Constants for regex patterns
const PATTERNS = {
EXAMPLE: /<Example\s+(?:(?:id="[^"]*"|component="[^"]*")\s*)+\s*\/>/g,
EXAMPLE_ID: /id="([^"]*)"/,
EXAMPLE_COMPONENT: /component="([^"]*)"/,
ANATOMY: /<Anatomy\s+id="[^"]*"\s*\/>/g,
COMPONENT_PREVIEW: /<ComponentPreview\s+id="[^"]*"\s*\/>/g,
FAQ: /## FAQ\s*<Faq\s*\/>/g,
IMAGES: /\/images/g,
} as const

// Quick links and templates
const TEMPLATES = {
NEXTJS: 'https://stackblitz.com/edit/github-qcm2dskf',
SOLID: 'https://stackblitz.com/edit/github-1hgkbbln',
NUXT: 'https://stackblitz.com/edit/github-s3sg6syq',
} as const

// Component replacement functions
const replaceQuickstart = () =>
[
`- [Next.js Template](${TEMPLATES.NEXTJS})`,
`- [Solid Start Template](${TEMPLATES.SOLID})`,
`- [Nuxt Template](${TEMPLATES.NUXT})`,
].join('\n')

const replaceInstallCmd = (framework: string) =>
[
'```bash',
Object.values(cmdMap)
.map((cmd) => `${cmd} @ark-ui/${framework}`)
.join('\n// or\n'),
'```',
].join('\n')

// Accessibility documentation
const replaceKeyboardTable = (id: string) => {
try {
const keyboardDoc = getAccessibilityDoc(id as AccessibilityDocKey)
return keyboardDoc.keyboard
.map((item) => {
return [`**\`${item.keys.join(' + ')}\`**`, `Description: ${item.description}`].join('\n')
})
.join('\n\n')
} catch {
return ''
}
}

type Part = (typeof types)[number]['parts'][keyof (typeof types)[number]['parts']]

// Component type formatting
const formatPropTypes = (props: NonNullable<Part['props']>) =>
Object.entries(props ?? {}).map(([propName, propDetails]) =>
[
`**\`${propName}\`**`,
`Type: \`${propDetails.type}\``,
`Required: ${propDetails.isRequired ? 'true' : 'false'}`,
`Default Value: \`${propDetails.defaultValue}\``,
`Description: ${propDetails.description}`,
].join('\n'),
)

const formatEmitTypes = (emits: NonNullable<Part['emits']>) =>
Object.entries(emits ?? {}).map(([emitName, emitDetails]) =>
[`**\`${emitName}\`**`, `Type: \`${emitDetails.type}\``, `Description: ${emitDetails.description}`].join('\n'),
)

const formatDataAttributes = (key: string, id: string) => {
try {
const dataAttrDoc = getDataAttrDoc(id as DataAttrDocKey)
const dataAttrForPart = dataAttrDoc[key as DataAttrDocKey]
return Object.entries(dataAttrForPart)
.map(([key, value]) => `**\`${key}\`**: ${value}`)
.join('\n')
} catch {
return ''
}
}

const formatComponentType = (key: string, part: Part, id: string) => {
const lines = [`### ${key}`, '#### Props', ...formatPropTypes(part.props)]

if (part.emits) {
lines.push('#### Emits', ...formatEmitTypes(part.emits))
}

const dataAttrItems = formatDataAttributes(key, id)
if (dataAttrItems) {
lines.push('#### Data Attributes', dataAttrItems)
}

return lines.join('\n\n')
}

const replaceComponentTypes = (id: string, framework: string) => {
const api = types.find((type) => type.component === id && type.framework === framework)
if (!api) return ''

return Object.entries(api.parts)
.sort(([key]) => (key === 'Root' ? -1 : 1))
.map(([key, part]) => formatComponentType(key, part, id))
.join('\n\n')
}

const replaceExamples = async (content: string, page: Pages, framework: string) => {
const examples = content.match(PATTERNS.EXAMPLE) || []
let res = content

for (const example of examples) {
const idMatch = example.match(PATTERNS.EXAMPLE_ID)
const componentMatch = example.match(PATTERNS.EXAMPLE_COMPONENT)

const id = idMatch?.[1]
if (!id) continue

const component = componentMatch?.[1] || page.slug.split('/')[1]
const { code, extension } = await frameworkExample(framework, component, id)
res = res.replace(example, `\`\`\`${extension}\n${code}\`\`\``)
}

return res
}

export const cleanupPageContent = async (page: Pages, framework: 'react' | 'solid' | 'vue' | 'svelte') => {
if (!page.llm) return ''
let res = page.llm

// Remove unwanted components
res = res.replace(PATTERNS.ANATOMY, '')
res = res.replace(PATTERNS.COMPONENT_PREVIEW, '')
res = res.replace(PATTERNS.FAQ, '')
res = res.replace(PATTERNS.IMAGES, 'https://ark-ui.com/images')

// Replace components with their content
res = res.replace(/<Quickstart\s*\/>/g, replaceQuickstart())
res = res.replace(/<InstallCmd\s*\/>/g, replaceInstallCmd(framework))
res = res.replace(/<KeyBindingsTable\s+id="([^"]*)"\s*\/>/g, (_, id) => replaceKeyboardTable(id))
res = res.replace(/<ComponentTypes\s+id="([^"]*)"\s*\/>/g, (_, id) => replaceComponentTypes(id, framework))

res = await replaceExamples(res, page, framework)

return res
}
6 changes: 3 additions & 3 deletions website/src/app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import type { MetadataRoute } from 'next'
import { fetchExamples } from '~/lib/examples'
import { getSidebarGroups } from '~/lib/sidebar'

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const frameworks = ['react', 'solid', 'vue']
export const frameworks = ['react', 'solid', 'vue'] as const

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const docsPages = frameworks.flatMap((framework) =>
getSidebarGroups()
.flat()
Expand All @@ -13,7 +13,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {

const examples = await fetchExamples()
const examplePages = frameworks.flatMap((framework) =>
examples.map((example) => ({ url: `https://ark-ui.com/${framework}/examples/${example.id}` })),
examples.map((example) => ({ url: `https://ark-ui.com/${framework}/examples/${example}` })),
)

return [{ url: 'https://ark-ui.com' }, ...docsPages, ...examplePages]
Expand Down
55 changes: 29 additions & 26 deletions website/src/components/example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,34 @@ export const Example = async (props: Props) => {
return <CodeTabs examples={examples} defaultValue={framework} />
}

export const frameworkExample = async (framework: string, component: string, id: string) => {
const extension = Match.value(framework).pipe(
Match.when('vue', () => 'vue'),
Match.orElse(() => 'tsx'),
)
const examplePath = Match.value(component).pipe(
Match.when(
() => ['progress-circular', 'progress-linear'].includes(component),
() => `components/progress/examples/${component.split('-')[1]}`,
),
Match.when(
() => ['environment', 'locale'].includes(component),
() => `providers/${component}/examples`,
),
Match.orElse(() => `components/${component}/examples`),
)

const basePath = `../packages/${framework}/src`
const fileName = [id, extension].join('.')

const content = await readFile(join(process.cwd(), basePath, examplePath, fileName), 'utf-8').catch(
() => 'Example not found',
)

const code = content.replaceAll(/from '\.\/icons'/g, `from 'lucide-vue-next'`).replace(/.*@ts-expect-error.*\n/g, '')
return { code, extension }
}

const findExamples = async (props: Props) => {
const id = props.id
const serverContext = getServerContext()
Expand All @@ -27,32 +55,7 @@ const findExamples = async (props: Props) => {

return Promise.all(
['react', 'solid', 'vue'].map(async (framework) => {
const extension = Match.value(framework).pipe(
Match.when('vue', () => 'vue'),
Match.orElse(() => 'tsx'),
)
const examplePath = Match.value(component).pipe(
Match.when(
() => ['progress-circular', 'progress-linear'].includes(component),
() => `components/progress/examples/${component.split('-')[1]}`,
),
Match.when(
() => ['environment', 'locale'].includes(component),
() => `providers/${component}/examples`,
),
Match.orElse(() => `components/${component}/examples`),
)

const basePath = `../packages/${framework}/src`
const fileName = [id, extension].join('.')

const content = await readFile(join(process.cwd(), basePath, examplePath, fileName), 'utf-8').catch(
() => 'Example not found',
)

const code = content
.replaceAll(/from '\.\/icons'/g, `from 'lucide-vue-next'`)
.replace(/.*@ts-expect-error.*\n/g, '')
const { code, extension } = await frameworkExample(framework, component, id)

const html = await codeToHtml(code, {
lang: extension,
Expand Down
Loading