mirror of
https://github.com/tabler/tabler.git
synced 2026-08-26 21:04:25 +04:00
Document llms.txt and the markdown page mirrors (#2904)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Fixed html comments in docs code examples running into the closing tag of the element before them.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": minor
|
||||
---
|
||||
|
||||
Added a Docs for LLMs page explaining `llms.txt` and the `.md` page mirrors, with a sidebar link.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Fixed the `.md` docs mirrors showing component tags like `<Icon />` instead of the rendered html.
|
||||
@@ -57,8 +57,8 @@ const innerClasses = ['p-6 w-full', column && 'd-flex gap-3 flex-column', !colum
|
||||
const highlighted = hideCode ? '' : await highlightCode(beautifyHtml(code ?? html), 'html')
|
||||
---
|
||||
|
||||
<!--EXAMPLE-->
|
||||
<div class="docs-example my-5">
|
||||
<!--EXAMPLE-->{/* data-example-markup carries the markup for examples with no code panel, so the .md mirror (docs/lib/llms.ts) still has real html to show */}
|
||||
<div class="docs-example my-5" data-example-markup={hideCode ? (code ?? html) : undefined}>
|
||||
{
|
||||
hasDemo && (
|
||||
<div class:list={exampleClasses} style={height ? `height: ${height}` : undefined}>
|
||||
|
||||
@@ -35,13 +35,7 @@ If you need to add icons to your website, you can use the [Tabler Icons library]
|
||||
|
||||
## Base icon
|
||||
|
||||
To add an icon to your code copy the SVG code from the Tabler Icons website and paste it into your HTML file.
|
||||
|
||||
```html
|
||||
<Icon name="heart" />
|
||||
```
|
||||
|
||||
Results can be seen in the example below.
|
||||
To add an icon to your code copy the SVG code from the Tabler Icons website and paste it into your HTML file. The example below shows a few icons; open its code panel for the markup.
|
||||
|
||||
<Example centered>
|
||||
<Icon name="heart" />
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
title: Docs for LLMs
|
||||
summary: The Tabler documentation is also published as plain markdown, so AI tools and agents can read it without parsing HTML.
|
||||
description: Use llms.txt and the markdown version of every Tabler docs page to give AI tools accurate, up-to-date context about Tabler.
|
||||
---
|
||||
|
||||
Large language models work better when they read the real documentation instead of guessing class names. Tabler publishes its docs in two machine-readable forms: one index file that lists every page, and a markdown copy of each page.
|
||||
|
||||
## Overview
|
||||
|
||||
[llms.txt](https://llmstxt.org) is a simple convention. A site puts one markdown file at its root that describes what the site is about and links to the pages worth reading. Tabler follows it:
|
||||
|
||||
- `https://docs.tabler.io/llms.txt` — the index of the whole documentation.
|
||||
- `https://docs.tabler.io/<page>.md` — the markdown version of a single page.
|
||||
|
||||
Both are plain text. You can open them in a browser, download them with `curl`, or paste the url into a chat with an AI assistant.
|
||||
|
||||
## The index file
|
||||
|
||||
Fetch the index to see the full map of the docs:
|
||||
|
||||
```shell
|
||||
curl https://docs.tabler.io/llms.txt
|
||||
```
|
||||
|
||||
It contains:
|
||||
|
||||
- A short description of what Tabler is.
|
||||
- Every documentation page, grouped the same way as the sidebar, with a one-line description and a link to its markdown file.
|
||||
- A class reference for every component that has one, so a model can look up which classes exist without opening each page.
|
||||
|
||||
In the class reference, a name in braces stands for a family of classes. `alert-{color}` means any base color, and `table-mobile-{breakpoint}` means any breakpoint.
|
||||
|
||||
## Markdown pages
|
||||
|
||||
Add `.md` to any docs url to get the same page as markdown:
|
||||
|
||||
```shell
|
||||
curl https://docs.tabler.io/ui/components/button.md
|
||||
```
|
||||
|
||||
The markdown file keeps the page title, the summary, the prose, and the markup of every example as fenced code blocks. It drops the navigation, the styling, and everything else that only matters in a browser.
|
||||
|
||||
The home page is the one exception. Use `/index.md`, because `/.md` is not a valid url.
|
||||
|
||||
Every docs page also links to these files in its `<head>`, so a crawler that already has the html can find them:
|
||||
|
||||
```html
|
||||
<link rel="alternate" type="text/plain" href="/llms.txt" title="llms.txt" />
|
||||
<link rel="alternate" type="text/markdown" href="/ui/components/button.md" title="Button (markdown)" />
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Point your tool at the file that fits the task:
|
||||
|
||||
- **Whole library.** Give the assistant `https://docs.tabler.io/llms.txt` when it needs to know what Tabler offers and which page to read next.
|
||||
- **One component.** Give it the page's `.md` url when you are working on a single component and want the exact markup.
|
||||
- **Local context.** Download the files into your project and commit them, so the same context is used by everyone on the team and by tools that cannot fetch urls.
|
||||
|
||||
A short prompt is usually enough:
|
||||
|
||||
```text
|
||||
Read https://docs.tabler.io/ui/components/card.md and build a card with a header, a footer and a table inside.
|
||||
```
|
||||
|
||||
## Where the markup comes from
|
||||
|
||||
The examples are taken from the rendered page, not from the documentation source. A `.md` file gives you the same markup as the copy button on the html page, with every icon and helper already expanded to plain html.
|
||||
@@ -332,6 +332,12 @@ const relatedPages = await Promise.all(
|
||||
<div class="flex-fill">
|
||||
<DocsMenu url={docsUrl} />
|
||||
</div>
|
||||
{/* Machine-readable index of the docs — see /ui/getting-started/llms */}
|
||||
<nav class="nav nav-vertical" aria-label="Docs for AI tools">
|
||||
<a class="nav-link" href="/llms.txt">
|
||||
<span class="nav-link-title">llms.txt</span>
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+64
-7
@@ -1,12 +1,21 @@
|
||||
// Source-level MDX → plain markdown, for the /llms.txt endpoints.
|
||||
// MDX → plain markdown, for the /llms.txt endpoints.
|
||||
//
|
||||
// The docs are MDX: prose is already markdown, but the parts that carry the most
|
||||
// value for a reader (the actual Tabler markup) sit inside <Example> slots and
|
||||
// component props. Stripping components wholesale — the usual llms.txt recipe —
|
||||
// would delete exactly that. So the components that hold content are unwrapped
|
||||
// into fenced code blocks instead, and only the decorative ones are dropped.
|
||||
//
|
||||
// Prose comes from the MDX source, but example markup comes from the *rendered*
|
||||
// page (see renderedExamples below): a third of the examples are written with
|
||||
// docs components — <Icon>, <AvatarList>, <Badge> — and the source form of those
|
||||
// is of no use to a reader who wants the html.
|
||||
import type { CollectionEntry } from 'astro:content'
|
||||
import { extractMarkedSnippet } from '@shared/lib/code-example'
|
||||
import { render } from 'astro:content'
|
||||
import { loadRenderers } from 'astro:container'
|
||||
import { experimental_AstroContainer as AstroContainer } from 'astro/container'
|
||||
import { getContainerRenderer } from '@astrojs/mdx/container-renderer'
|
||||
import { beautifyHtml, extractMarkedSnippet } from '@shared/lib/code-example'
|
||||
import { site } from '@shared/lib/site'
|
||||
import packageManagers from '@data/package-managers.json'
|
||||
import { cdnCssTag, cdnJsTag, cdnPackageSnippet, cdnPluginSnippet } from './cdn-snippets.ts'
|
||||
@@ -110,8 +119,53 @@ function restoreCode(text: string, store: string[]) {
|
||||
return result
|
||||
}
|
||||
|
||||
/** Turn one page's MDX body into plain markdown. */
|
||||
export async function mdxToMarkdown(body: string): Promise<string> {
|
||||
// Marker Example.astro puts in front of every example it renders.
|
||||
const EXAMPLE_MARKER = '<!--EXAMPLE-->'
|
||||
|
||||
const decodeEntities = (value: string) =>
|
||||
value
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/&/g, '&')
|
||||
|
||||
// One container for the whole build: creating it loads the MDX renderer.
|
||||
let container: Promise<AstroContainer> | undefined
|
||||
|
||||
const getContainer = () => (container ??= loadRenderers([getContainerRenderer()]).then((renderers) => AstroContainer.create({ renderers })))
|
||||
|
||||
/**
|
||||
* The final markup of every <Example> on a page, in document order — the same
|
||||
* html the page's copy button hands out, so components are already expanded.
|
||||
* An entry is null only when the example renders nothing, and the whole list is
|
||||
* empty when the page cannot be rendered.
|
||||
*/
|
||||
async function renderedExamples(entry: CollectionEntry<'docs'>): Promise<(string | null)[]> {
|
||||
try {
|
||||
const { Content } = await render(entry)
|
||||
const html = await (await getContainer()).renderToString(Content)
|
||||
|
||||
return html
|
||||
.split(EXAMPLE_MARKER)
|
||||
.slice(1)
|
||||
.map((block) => {
|
||||
// the copy button, or the wrapper attribute when the example hides its code panel
|
||||
const markup = block.match(/data-clipboard-text="([^"]*)"/) ?? block.match(/data-example-markup="([^"]*)"/)
|
||||
return markup ? beautifyHtml(decodeEntities(markup[1]!)) : null
|
||||
})
|
||||
} catch {
|
||||
// A page that fails to render still gets its prose and its source-level examples.
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn one page's MDX body into plain markdown. `examples` comes from
|
||||
* renderedExamples() and replaces the source of each <Example> slot; it is
|
||||
* ignored unless it lines up one-to-one with the examples in the source.
|
||||
*/
|
||||
export async function mdxToMarkdown(body: string, examples: (string | null)[] = []): Promise<string> {
|
||||
// `<Code lang code={`…`} />` first: its template literal contains backticks, which
|
||||
// would otherwise be mistaken for markdown code spans by protectCode() below.
|
||||
let text = body.replace(/<Code\b[^>]*?code=\{`([\s\S]*?)`\}[\s\S]*?\/>/g, (match, snippet: string) => {
|
||||
@@ -127,8 +181,11 @@ export async function mdxToMarkdown(body: string): Promise<string> {
|
||||
text = text.replace(/^import\s+.+?from\s+['"][^'"]+['"];?[ \t]*$/gm, '')
|
||||
|
||||
// <Example> slots hold the markup the page is actually documenting
|
||||
text = text.replace(/<Example\b[^>]*>([\s\S]*?)<\/Example>/g, (_match, inner: string) => {
|
||||
const snippet = dedent(inner)
|
||||
const examplePattern = /<Example\b[^>]*>([\s\S]*?)<\/Example>/g
|
||||
const useRendered = examples.length === (text.match(examplePattern)?.length ?? 0)
|
||||
let exampleIndex = 0
|
||||
text = text.replace(examplePattern, (_match, inner: string) => {
|
||||
const snippet = (useRendered ? examples[exampleIndex++] : null) ?? dedent(inner)
|
||||
return snippet ? `\n${fence(snippet)}\n` : ''
|
||||
})
|
||||
|
||||
@@ -170,7 +227,7 @@ export async function pageMarkdown(entry: CollectionEntry<'docs'>, url: string):
|
||||
const { title, summary, description } = entry.data
|
||||
const header = [`# ${title}`, '', `> ${summary}`, '', description, '', `Source: ${url}`, '', '---', ''].join('\n')
|
||||
|
||||
return `${header}\n${await mdxToMarkdown(entry.body ?? '')}\n`
|
||||
return `${header}\n${await mdxToMarkdown(entry.body ?? '', await renderedExamples(entry))}\n`
|
||||
}
|
||||
|
||||
/** Absolute in production, root-relative in dev — same rule as sitemap.xml.ts. */
|
||||
|
||||
@@ -87,7 +87,7 @@ Tabler is a free and open source dashboard UI kit built on Bootstrap. This file
|
||||
|
||||
Every page is available as markdown by appending \`.md\` to its url, for example \`${site.docsUrl}/ui/components/button.md\`. Those files contain the same prose as the html page plus the markup of every example, as fenced code blocks.
|
||||
|
||||
The markup is taken from the documentation source. A small number of examples are written with Tabler's own documentation components, and those appear as component tags (for example \`<Alert type="success" />\`) instead of the final html; open the page itself for those.
|
||||
The markup is taken from the rendered pages, so every example is plain html — the same markup the copy button on the page gives you.
|
||||
|
||||
${sections.join('\n\n')}
|
||||
|
||||
|
||||
@@ -475,6 +475,10 @@
|
||||
"title": "FAQ",
|
||||
"url": "/ui/getting-started/faq"
|
||||
},
|
||||
{
|
||||
"title": "Docs for LLMs",
|
||||
"url": "/ui/getting-started/llms"
|
||||
},
|
||||
{
|
||||
"title": "3rd-party libraries & resources",
|
||||
"url": "/ui/getting-started/references"
|
||||
|
||||
@@ -11,6 +11,12 @@ describe('beautifyHtml', () => {
|
||||
// between sibling elements — so line breaks have to be added back.
|
||||
expect(beautifyHtml('<div><span>x</span></div>')).toBe('<div>\n <span>x</span>\n</div>')
|
||||
})
|
||||
|
||||
it('starts a comment that follows an element on its own line', () => {
|
||||
// Icon examples are a comment plus an svg, repeated — without the break the
|
||||
// next comment ends up on the closing tag of the icon before it.
|
||||
expect(beautifyHtml('<svg></svg><!-- next --><svg></svg>')).toBe('<svg></svg>\n<!-- next -->\n<svg></svg>')
|
||||
})
|
||||
})
|
||||
|
||||
describe('highlightCode', () => {
|
||||
|
||||
@@ -13,7 +13,12 @@ function getHighlighter(): Promise<Highlighter> {
|
||||
}
|
||||
|
||||
export function beautifyHtml(code: string): string {
|
||||
return beautify.html(code, {
|
||||
// A comment that follows an element stays glued to it — js-beautify never adds
|
||||
// the break itself. Icon examples arrive as one line, so every "Download SVG
|
||||
// icon" comment would sit on the closing tag of the icon before it.
|
||||
const withCommentBreaks = code.replace(/>\s*<!--/g, '>\n<!--')
|
||||
|
||||
return beautify.html(withCommentBreaks, {
|
||||
indent_size: 2,
|
||||
indent_char: ' ',
|
||||
max_preserve_newlines: 5,
|
||||
|
||||
Reference in New Issue
Block a user