Skip to content
Closed
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
11 changes: 11 additions & 0 deletions site/src/content/docs/getting-started/install.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,14 @@ You can also do the old fashioned thing and download Bootstrap manually. Choose
</BsTable>

Should you require our full set of [build tools]([[docsref:/guides/contribute#tooling-setup]]), they are included for developing Bootstrap and its docs, but they’re likely unsuitable for your own purposes.

## For AI tools & LLMs

Our documentation is available in a machine-readable format for large language models and AI-powered tools, following the [llms.txt](https://llmstxt.org/) convention.

<BsTable>
| File | Description | Link |
| --- | --- | --- |
| `llms.txt` | A curated index of every documentation page with links and descriptions. | [/llms.txt](/llms.txt) |
| `llms-full.txt` | The full text of the documentation concatenated into a single file for direct ingestion. | [/llms-full.txt](/llms-full.txt) |
</BsTable>
88 changes: 88 additions & 0 deletions site/src/libs/llms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { docsPages, getDocsPageSlug } from '@libs/content'
import { getData, type SidebarItem, type SidebarSubItem } from '@libs/data'
import { getVersionedDocsPath } from '@libs/path'
import { getSlug } from '@libs/utils'

export interface LlmsPage {
title: string
description: string
// The versioned docs path, e.g. `/docs/6.0/getting-started/install`.
path: string
// The raw MDX body of the page, if available.
body: string
}

export interface LlmsSection {
heading: string
pages: LlmsPage[]
}

// Resolve a sidebar entry (by its group slug and title) to a docs collection
// entry, mirroring the slug logic in `DocsSidebar.astro`. Unlike the sidebar,
// this stays resilient and returns `undefined` when there is no match instead
// of throwing, so the generated `llms.txt` files never fail a build over a
// single stray sidebar entry.
function resolvePage(groupSlug: string, title: string | undefined): LlmsPage | undefined {
if (!title) {
return undefined
}

const unversionedPageSlug = `${groupSlug}/${getSlug(title)}`
const entry = docsPages.find((page) => getDocsPageSlug(page.id) === unversionedPageSlug)

if (!entry) {
return undefined
}

return {
title: entry.data.title,
description: entry.data.description,
path: getVersionedDocsPath(unversionedPageSlug),
body: entry.body ?? ''
}
}

// Build an ordered, grouped list of docs pages from `site/data/sidebar.yml`.
// Both `/llms.txt` and `/llms-full.txt` consume this so they stay in sync with
// the site navigation.
export function getLlmsSections(): LlmsSection[] {
const sidebar = getData('sidebar')
const sections: LlmsSection[] = []

for (const group of sidebar) {
if (!group.pages) {
continue
}

const groupSlug = getSlug(group.title)
const pages: LlmsPage[] = []

for (const item of group.pages as SidebarItem[]) {
// Nested sub-group (e.g. Utilities): flatten its pages under the parent
// group heading.
if (item.group && item.pages) {
for (const subPage of item.pages as SidebarSubItem[]) {
const resolved = resolvePage(groupSlug, subPage.title)

if (resolved) {
pages.push(resolved)
}
}

continue
}

const resolved = resolvePage(groupSlug, item.title)

if (resolved) {
pages.push(resolved)
}
}

if (pages.length > 0) {
sections.push({ heading: group.title, pages })
}
}

return sections
}
39 changes: 39 additions & 0 deletions site/src/pages/llms-full.txt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { APIRoute } from 'astro'
import { getConfig } from '@libs/config'
import { getLlmsSections } from '@libs/llms'

// Strip leading MDX `import ... from '...'` statements so the concatenated
// output reads as prose. Shortcode tags (e.g. `<Example>`, `<Callout>`) are
// left inline — full fidelity isn't achievable from raw MDX without rendering,
// but the content stays highly usable for ingestion.
function cleanBody(body: string): string {
return body.replace(/^import\s.+?\n/gm, '').trim()
}

// Generates `/llms-full.txt`, the full text of the documentation concatenated
// into a single file for direct ingestion by LLMs. See https://llmstxt.org/.
export const GET: APIRoute = function GET({ site }) {
const { title, description } = getConfig()
const sections = getLlmsSections()

const lines: string[] = [`# ${title}`, '', `> ${description}`, '']

for (const section of sections) {
for (const page of section.pages) {
const url = new URL(page.path, site)
const body = cleanBody(page.body)

lines.push('---', '', `# ${page.title}`, '', `Source: ${url}`, '', page.description, '')

if (body.length > 0) {
lines.push(body, '')
}
}
}

return new Response(`${lines.join('\n').trimEnd()}\n`, {
headers: {
'Content-Type': 'text/plain; charset=utf-8'
}
})
}
35 changes: 35 additions & 0 deletions site/src/pages/llms.txt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { APIRoute } from 'astro'
import { getConfig } from '@libs/config'
import { getLlmsSections } from '@libs/llms'

// Generates `/llms.txt`, a curated index of the docs for LLMs, following the
// llmstxt.org format: an H1 title, a blockquote summary, then grouped lists of

Check failure on line 6 in site/src/pages/llms.txt.ts

View workflow job for this annotation

GitHub Actions / cspell

Unknown word (llmstxt)
// links with descriptions. See https://llmstxt.org/.
export const GET: APIRoute = function GET({ site }) {
const { title, description } = getConfig()
const sections = getLlmsSections()

const lines: string[] = [`# ${title}`, '', `> ${description}`, '']

lines.push(
`The full text of the documentation is available at [${new URL('llms-full.txt', site)}](${new URL('llms-full.txt', site)}).`,
''
)

for (const section of sections) {
lines.push(`## ${section.heading}`, '')

for (const page of section.pages) {
const url = new URL(page.path, site)
lines.push(`- [${page.title}](${url}): ${page.description}`)
}

lines.push('')
}

return new Response(`${lines.join('\n').trimEnd()}\n`, {
headers: {
'Content-Type': 'text/plain; charset=utf-8'
}
})
}
Loading