From cb01a2cc370d447222fa542b9d346ecc2e01bcf0 Mon Sep 17 00:00:00 2001 From: Scott Rhamy Date: Sun, 4 Jan 2026 16:13:12 -0500 Subject: [PATCH 01/39] Open in LLM feature --- docs/src/lib/components/OpenLLMs.svelte | 97 +++++++++++++++++++ .../docs/components/[name]/+layout.svelte | 3 + docs/vite.config.ts | 4 +- 3 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 docs/src/lib/components/OpenLLMs.svelte diff --git a/docs/src/lib/components/OpenLLMs.svelte b/docs/src/lib/components/OpenLLMs.svelte new file mode 100644 index 000000000..deb1a0dc8 --- /dev/null +++ b/docs/src/lib/components/OpenLLMs.svelte @@ -0,0 +1,97 @@ + + + + + diff --git a/docs/src/routes/docs/components/[name]/+layout.svelte b/docs/src/routes/docs/components/[name]/+layout.svelte index 6d4b92132..598943356 100644 --- a/docs/src/routes/docs/components/[name]/+layout.svelte +++ b/docs/src/routes/docs/components/[name]/+layout.svelte @@ -2,6 +2,7 @@ import { getSettings } from 'layerchart'; import { Button, Menu, Switch, Toggle, ToggleGroup, ToggleOption, Tooltip } from 'svelte-ux'; import { toTitleCase } from '@layerstack/utils'; + import OpenLLMsbutton from '$lib/components/OpenLLMs.svelte'; import ViewSourceButton from '$lib/components/ViewSourceButton.svelte'; import { examples } from '$lib/context.js'; @@ -138,6 +139,8 @@ /> {/if} + + ` } } }) /*, devtoolsJson()*/ From d11704b0854d1b2346270eb46b46f73341c2c7b8 Mon Sep 17 00:00:00 2001 From: Scott Rhamy Date: Sun, 4 Jan 2026 16:51:51 -0500 Subject: [PATCH 02/39] removed grok --- docs/src/lib/components/OpenLLMs.svelte | 20 -------------------- docs/vite.config.ts | 4 +--- 2 files changed, 1 insertion(+), 23 deletions(-) diff --git a/docs/src/lib/components/OpenLLMs.svelte b/docs/src/lib/components/OpenLLMs.svelte index deb1a0dc8..e6faa322a 100644 --- a/docs/src/lib/components/OpenLLMs.svelte +++ b/docs/src/lib/components/OpenLLMs.svelte @@ -5,10 +5,7 @@ import ChevronDownIcon from '~icons/lucide/chevron-down'; import SimpleIconsOpenai from '~icons/simple-icons/openai'; import SimpleIconsClaude from '~icons/simple-icons/claude'; - import CustomGrokIcon from '~icons/custom-brands/grok'; import ClipboardIcon from '~icons/lucide/clipboard-copy'; - // import CustomDeepseekIcon from '~icons/custom-brands/deepseek'; - // import SimpleIconsGemini from '~icons/simple-icons/googlegemini'; let { buttonLabel = 'LLM Chat' } = $props(); @@ -37,23 +34,6 @@ value: generateLlmUrl('https://claude.ai/new?q='), icon: SimpleIconsClaude }, - /* Gemini does not support native URL query params */ - // { - // label: 'Open in Gemini', - // value: generateLlmUrl('https://google.com'), - // icon: SimpleIconsGemini - // }, - { - label: 'Open in Grok', - value: generateLlmUrl('https://grok.com/?q='), - icon: CustomGrokIcon - }, - /* DeepSeek does not support native URL query params */ - // { - // label: 'Open in DeepSeek', - // value: generateLlmUrl('https://deepseek.com/?q='), - // icon: CustomDeepseekIcon - // }, { label: 'Copy Page URL to Clipboard', value: null, diff --git a/docs/vite.config.ts b/docs/vite.config.ts index c19895e39..fbdfa304b 100644 --- a/docs/vite.config.ts +++ b/docs/vite.config.ts @@ -20,9 +20,7 @@ export default defineConfig({ svelteux: ``, shadcnsvelte: ``, skeleton: ``, - daisyUI: ``, - grok: `Grok` - // deepseek: `` + daisyUI: `` } } }) /*, devtoolsJson()*/ From 9867588093fd3877899c8862811e987c7aba24d0 Mon Sep 17 00:00:00 2001 From: Scott Rhamy Date: Wed, 7 Jan 2026 18:10:36 -0500 Subject: [PATCH 03/39] open-in-llm-component - rewrote copy for LLMs --- docs/src/lib/components/DocsMenu.svelte | 4 +- docs/src/lib/components/OpenLLMs.svelte | 175 +++++++++++++----- .../docs/components/[name]/+layout.svelte | 11 +- docs/src/routes/docs/guides/LLMs/+page.md | 35 ++++ 4 files changed, 166 insertions(+), 59 deletions(-) create mode 100644 docs/src/routes/docs/guides/LLMs/+page.md diff --git a/docs/src/lib/components/DocsMenu.svelte b/docs/src/lib/components/DocsMenu.svelte index 49b3b95cf..b06c8e2d5 100644 --- a/docs/src/lib/components/DocsMenu.svelte +++ b/docs/src/lib/components/DocsMenu.svelte @@ -7,6 +7,7 @@ import { sortFunc } from '@layerstack/utils'; import { cls } from '@layerstack/tailwind'; + import LucideBot from '~icons/lucide/bot'; import LucideCompass from '~icons/lucide/compass'; import LucideGalleryVertical from '~icons/lucide/gallery-vertical'; import LucideGalleryHorizontalEnd from '~icons/lucide/gallery-horizontal-end'; @@ -28,7 +29,8 @@ { name: 'Simplified charts', path: 'simplified-charts' }, { name: 'Scales', path: 'scales' }, { name: 'State', path: 'state' }, - { name: 'Styles', path: 'styles' } + { name: 'Styles', path: 'styles' }, + { name: 'LLMs', path: 'LLMs' } ]; const componentsByCategory = flatGroup(allComponents, (d) => d.category?.toLowerCase()) diff --git a/docs/src/lib/components/OpenLLMs.svelte b/docs/src/lib/components/OpenLLMs.svelte index e6faa322a..64a9a330f 100644 --- a/docs/src/lib/components/OpenLLMs.svelte +++ b/docs/src/lib/components/OpenLLMs.svelte @@ -1,77 +1,156 @@ - - + + + + + (openSourceModal = false)} + class="max-h-[98dvh] md:max-h-[90dvh] max-w-[98vw] md:max-w-[90vw] grid grid-rows-[auto_1fr_auto]" +> +
+
+
Source
+
{metadata?.sourceUrl}
- - {#each llms as llm} - { - toggleOff(); - if (llm.onclick) { - llm.onclick(); - } else { - window.open(llm.value, '_blank'); - } - }} - > - - {llm.label} - - {/each} - - - + + {#if metadata?.sourceUrl} + + {/if} +
+ +
+ +
+ +
+ +
+
diff --git a/docs/src/routes/docs/components/[name]/+layout.svelte b/docs/src/routes/docs/components/[name]/+layout.svelte index 598943356..3371fa84a 100644 --- a/docs/src/routes/docs/components/[name]/+layout.svelte +++ b/docs/src/routes/docs/components/[name]/+layout.svelte @@ -130,16 +130,7 @@
{metadata.description}
- {#if 'source' in metadata} - - {/if} - - + +{#if data.metadata.title !== 'LLMs'} +
+ +
+{/if} + + + {#snippet pending()} + loading... + {/snippet} + + {@render children()} + diff --git a/docs/src/routes/docs/guides/+layout.ts b/docs/src/routes/docs/guides/+layout.ts new file mode 100644 index 000000000..c4b1c5d61 --- /dev/null +++ b/docs/src/routes/docs/guides/+layout.ts @@ -0,0 +1,26 @@ +import { toTitleCase } from '@layerstack/utils'; + +// Dynamically import all guide markdown files to access their frontmatter +const modules = import.meta.glob('./**/+page.md', { eager: true }) as Record< + string, + { metadata?: Record } +>; + +export const load = async ({ url }) => { + // Get the last segment of the path (e.g., "features" from "/docs/guides/features") + const segments = url.pathname.split('/').filter(Boolean); + const name = segments[segments.length - 1]; + + // Get metadata from the markdown file's frontmatter + const modulePath = `./${name}/+page.md`; + const module = modules[modulePath]; + const frontmatter = module?.metadata ?? {}; + + // Use frontmatter title if available, otherwise derive from path + const title = + (frontmatter.title as string) ?? toTitleCase(name.replaceAll('-', ' ')); + + return { + metadata: { ...frontmatter, title } + }; +}; diff --git a/docs/src/routes/docs/guides/LLMs/+page.md b/docs/src/routes/docs/guides/LLMs/+page.md index c4f5df52b..923b6c114 100644 --- a/docs/src/routes/docs/guides/LLMs/+page.md +++ b/docs/src/routes/docs/guides/LLMs/+page.md @@ -1,33 +1,42 @@ +--- +title: LLMs +--- + -# LLMs Documentation - -The Layerchart documentation pages are designed to be accessible for humans developers using LLMs and large language models (LLMs) for effective content ingestion and training. +The Layerchart documentation pages are designed to be accessible for humans developers using LLMs as well as large language models (LLMs) ingestesting training data. -## For the Humans :icon{name="lucide:user" class="inline-block relative -top-0.5 left-0.5 w-7 h-7"} +## :icon{name="lucide:user" class="relative -top-1"} For the Humans - + At the top of each documentation page, and demonstrated above, you'll find a button which copies the content of the page's documentation in Markdown to the clipboard. The convenient dropdown gives you addtional helpful options. -## For the Bots :icon{name="lucide:bot" class="inline-block relative -top-0.5 left-0.5"} +::note +The option for `View Component Source` is only shown for component pages. +:: + +## :icon{name="lucide:bot" class="relative -top-1"} For the Bots LayerChart adopts the [llms.txt](https://llmstxt.org/) proposal standard, which provides a structured, machine-readable format optimized for LLMs. This enables developers, researchers, and AI systems to efficiently parse and utilize our documentation. ::note -Not all pages may support the `/llms.txt` suffix (those deemed irrelevant to LLMs). +Not all pages may support the `/llms.txt` suffix (ie those deemed irrelevant to LLMs). :: -## Accessing LLM-friendly Documentation +## LLM-friendly Documentation 3 Ways -1. To access the LLM-friendly version of supported Layerchart documentation pages, simply append `/llms.txt` to the end of the page's URL. This will return the content in a plain-text, LLM-optimized format. +1. To access the LLM-friendly version of supported Layerchart documentation pages, simply append `/llms.txt` to the end of the page's URL. This will return the content in a plain-text, LLM-optimized format. This is the same text which is copied to the clipboard when you click the `View Page Markdown` button. - - **Standard Page**: The LineChart component documentation is available at [layerchart.com/docs/components/LineChart](/docs/components/LineChart). - - **LLM-friendly Version**: Append `/llms.txt` to access it at [layerchart.com/docs/components/Linechart/llms.txt](/docs/components/LineChart/llms.txt). +:::tip +- **Standard Page**: The LineChart component documentation is available at [layerchart.com/docs/components/LineChart](/docs/components/LineChart). + +- **LLM-friendly Version**: is available at [layerchart.com/docs/components/Linechart/llms.txt](/docs/components/LineChart/llms.txt). +::: -2. To explore all supported pages in LLM-friendly format, visit the root index at [layerchart.com/llms.txt](llms.txt). This page provides a comprehensive list of available documentation endpoints compatible with the `llms.txt` standard. +1. To explore all supported pages in LLM-friendly format, visit the root index at [layerchart.com/llms.txt](/llms.txt). This page provides a comprehensive list of available documentation endpoints compatible with the `llms.txt` standard. -3. For a complete, consolidated view of the Layerchart documentation in an LLM-friendly format, navigate to [layerchart.com/docs/llms.txt](/docs/llms.txt). This single endpoint aggregates all documentation content into a machine-readable structure, ideal for bulk processing or ingestion into AI systems. +1. For a complete, consolidated view of the all the Layerchart documentation in an LLM-friendly format, navigate to [layerchart.com/docs/llms.txt](/docs/llms.txt). This single endpoint aggregates all documentation content into a machine-readable structure, ideal for bulk processing or ingestion into AI systems. diff --git a/docs/src/routes/docs/guides/[name]/llms.txt/+server.ts b/docs/src/routes/docs/guides/[name]/llms.txt/+server.ts new file mode 100644 index 000000000..ba264cc3a --- /dev/null +++ b/docs/src/routes/docs/guides/[name]/llms.txt/+server.ts @@ -0,0 +1,34 @@ +import { error } from '@sveltejs/kit'; +import { readFileSync } from 'fs'; +import { join } from 'path'; +import type { RequestHandler } from './$types'; +import { processMarkdownContent } from '$lib/markdown/utils.js'; + +export const GET: RequestHandler = async ({ params }) => { + const { name } = params; + + let content = ''; + try { + const mdPath = join(process.cwd(), `src/routes/docs/guides/${name}/+page.md`); + content = readFileSync(mdPath, 'utf-8'); + } catch (e) { + error(404, `Guide "${name}" not found`); + } + + content = processMarkdownContent(content); + + // Generate title from name + const title = name + .split('-') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); + + const markdown = `# ${title}\n\n${content}`; + + return new Response(markdown, { + headers: { + 'Content-Type': 'text/markdown; charset=utf-8', + 'Content-Disposition': `inline; filename="${name}.md"` + } + }); +}; diff --git a/docs/src/routes/docs/guides/features/+page.md b/docs/src/routes/docs/guides/features/+page.md index 314bed042..b3c74ad62 100644 --- a/docs/src/routes/docs/guides/features/+page.md +++ b/docs/src/routes/docs/guides/features/+page.md @@ -1,4 +1,6 @@ -# Features +--- +title: Features +--- > WIP diff --git a/docs/src/routes/docs/guides/layers/+page.md b/docs/src/routes/docs/guides/layers/+page.md index 7f6cfe5de..dc38fc21d 100644 --- a/docs/src/routes/docs/guides/layers/+page.md +++ b/docs/src/routes/docs/guides/layers/+page.md @@ -1,11 +1,13 @@ +--- +title: Layers +--- + -# Layers - LayerChart provides first-class support for different types of layers including [Svg](/docs/components/Svg), [Html](/docs/components/Html), and [Canvas](/docs/components/Canvas) via [Layer](/docs/components/Layer) and [Primitive](/docs/guides/primitives) components. Each layer type provides unique and overlapping feature sets. LayerChart supports using layers of different types within the same chart to leverage a type's strengths or workaround a weakness. diff --git a/docs/src/routes/docs/guides/primitives/+page.md b/docs/src/routes/docs/guides/primitives/+page.md index 1b332b23f..96cee2a8f 100644 --- a/docs/src/routes/docs/guides/primitives/+page.md +++ b/docs/src/routes/docs/guides/primitives/+page.md @@ -1,3 +1,7 @@ +--- +title: Primitives +--- + -# Primitives - A collection of components which support rendering within different layer types including `Svg`, `Canvas`, or `Html`. Support for each layer type is dependent on the primitive's feature needs and capabilities of the layer type. diff --git a/docs/src/routes/docs/guides/scales/+page.md b/docs/src/routes/docs/guides/scales/+page.md index 4ed81aa27..ebe8db295 100644 --- a/docs/src/routes/docs/guides/scales/+page.md +++ b/docs/src/routes/docs/guides/scales/+page.md @@ -1,3 +1,7 @@ +--- +title: Scales +--- + -# Scales - ## What is a scale? At its essence, a scale is a function that maps data values (`domain`) to pixel or color values (`range`) on a per-dimension basis (`x`, `y`, `color`, etc). diff --git a/docs/src/routes/docs/guides/simplified-charts/+page.md b/docs/src/routes/docs/guides/simplified-charts/+page.md index efe17850c..9a2ae6981 100644 --- a/docs/src/routes/docs/guides/simplified-charts/+page.md +++ b/docs/src/routes/docs/guides/simplified-charts/+page.md @@ -1,3 +1,7 @@ +--- +title: Simplified Charts +--- + -# Simplified charts - The LayerChart project was written to offer options for both flexibility/complexity as well as approachablilty/simplicity. This brings us to a decision as you start your first LayerChart. ## Use `` or `Simple Chart`. diff --git a/docs/src/routes/docs/guides/state/+page.md b/docs/src/routes/docs/guides/state/+page.md index f48eca287..b24bf03c5 100644 --- a/docs/src/routes/docs/guides/state/+page.md +++ b/docs/src/routes/docs/guides/state/+page.md @@ -1,4 +1,6 @@ -# State / Context +--- +title: State / Context +--- > TODO diff --git a/docs/src/routes/docs/guides/styles/+page.md b/docs/src/routes/docs/guides/styles/+page.md index f84da58aa..8445269fa 100644 --- a/docs/src/routes/docs/guides/styles/+page.md +++ b/docs/src/routes/docs/guides/styles/+page.md @@ -1,4 +1,6 @@ -# Styling +--- +title: Styling +--- ## Colors diff --git a/docs/src/routes/docs/llms.txt/+server.ts b/docs/src/routes/docs/llms.txt/+server.ts new file mode 100644 index 000000000..96e27b2bf --- /dev/null +++ b/docs/src/routes/docs/llms.txt/+server.ts @@ -0,0 +1,237 @@ +import { readFileSync } from 'fs'; +import { join } from 'path'; +import type { RequestHandler } from './$types'; +import type { ComponentAPI } from '$lib/api-types.js'; +import { allComponents, allUtils } from 'content-collections'; + +const BASE_URL = 'https://layerchart.com'; + +const guides = [ + { slug: 'getting-started', name: 'Getting Started', description: 'Installation and setup guide for LayerChart' }, + { slug: 'guides/scales', name: 'Scales', description: 'Understanding scales, domains, and ranges' }, + { slug: 'guides/layers', name: 'Layers', description: 'Working with SVG, Canvas, and HTML layers' }, + { slug: 'guides/state', name: 'State', description: 'Managing chart state and reactivity' }, + { slug: 'guides/styles', name: 'Styles', description: 'Styling and theming your charts' }, + { slug: 'guides/primitives', name: 'Primitives', description: 'Low-level building blocks for custom charts' }, + { slug: 'guides/simplified-charts', name: 'Simplified Charts', description: 'Quick chart components for common use cases' }, + { slug: 'guides/features', name: 'Features', description: 'Overview of LayerChart features and capabilities' } +]; + +export const GET: RequestHandler = async () => { + const content = generateFullLlmsTxt(); + + return new Response(content, { + headers: { + 'Content-Type': 'text/plain; charset=utf-8' + } + }); +}; + +function generateFullLlmsTxt(): string { + const sections: string[] = []; + + // Header + sections.push(`# LayerChart Full Documentation for LLMs + +> LayerChart is a powerful, composable charting library for Svelte built on top of D3. + +This file contains the complete LLM-optimized documentation for all components and utilities.`); + + // General section (links only, as these are markdown pages) + sections.push(`## General + +${guides.map((g) => `- [${g.name}](${BASE_URL}/docs/${g.slug}): ${g.description}`).join('\n')}`); + + // Components section - full content + sections.push('---\n\n# Components'); + + const sortedComponents = allComponents + .filter((c) => c.slug && c.name) + .sort((a, b) => a.name.localeCompare(b.name)); + + for (const component of sortedComponents) { + const componentContent = generateComponentMarkdown(component); + sections.push(componentContent); + } + + // Utilities section - full content + sections.push('---\n\n# Utilities'); + + const sortedUtils = allUtils + .filter((u) => u.slug && u.name) + .sort((a, b) => a.name.localeCompare(b.name)); + + for (const util of sortedUtils) { + const utilContent = generateUtilMarkdown(util); + sections.push(utilContent); + } + + return sections.join('\n\n'); +} + +/** + * Trim code to remove module exports and data export statement + */ +function trimCode(code: string): string { + return code + .replace(/[\s\S]*?<\/script>\n*/g, '') + .replace(/\n*\s*export \{ data \};\s*\n*\s*<\/script>/gm, '\n') + .trim(); +} + +/** + * Escape special markdown characters in table cells + */ +function escapeMarkdown(text: string): string { + return text + .replace(/\|/g, '\\|') + .replace(/\n/g, ' ') + .replace(//g, '>'); +} + +/** + * Generate markdown API table from component properties + */ +function generateApiTable(api: ComponentAPI): string { + if (!api.properties || api.properties.length === 0) { + return ''; + } + + const rows = api.properties.map((prop) => { + const name = prop.required ? `**${prop.name}** (required)` : prop.name; + const type = `\`${escapeMarkdown(prop.type)}\``; + const defaultVal = prop.default ? `\`${escapeMarkdown(prop.default)}\`` : '-'; + const description = prop.description ? escapeMarkdown(prop.description) : '-'; + return `| ${name} | ${type} | ${defaultVal} | ${description} |`; + }); + + return `| Property | Type | Default | Description | +|----------|------|---------|-------------| +${rows.join('\n')}`; +} + +/** + * Generate markdown for a component + */ +function generateComponentMarkdown(component: (typeof allComponents)[number]): string { + const sections: string[] = []; + + // Title and description + sections.push(`## ${component.name}`); + if (component.description) { + sections.push(component.description); + } + + // Metadata + if (component.category) { + sections.push(`**Category:** ${component.category}`); + } + if (component.layers && component.layers.length > 0) { + sections.push(`**Supported Layers:** ${component.layers.join(', ')}`); + } + + // Documentation link + sections.push(`**Full Documentation:** [${component.name}](${BASE_URL}/docs/components/${component.slug})`); + + // Load example + let exampleSource = ''; + if (component.usageExample) { + try { + const examplePath = join( + process.cwd(), + `src/examples/components/${component.slug}/${component.usageExample}.svelte` + ); + exampleSource = readFileSync(examplePath, 'utf-8'); + exampleSource = trimCode(exampleSource); + } catch (e) { + // Example file may not exist + } + } + + if (exampleSource) { + sections.push('### Example'); + sections.push('```svelte\n' + exampleSource + '\n```'); + } + + // Load API + let api: ComponentAPI | null = null; + try { + const apiPath = join(process.cwd(), `src/generated/api/${component.slug}.json`); + const apiContent = readFileSync(apiPath, 'utf-8'); + api = JSON.parse(apiContent); + } catch (e) { + // API file may not exist + } + + if (api) { + sections.push('### API'); + const table = generateApiTable(api); + if (table) { + sections.push(table); + } + + if (api.extends && api.extends.length > 0) { + const extendsList = api.extends.map((e) => `\`${e.fullType}\``).join(', '); + sections.push(`**Extends:** ${extendsList}`); + } + } + + // Related + if (component.related && component.related.length > 0) { + sections.push('### Related'); + const relatedLinks = component.related + .map((r) => `- [${r}](${BASE_URL}/docs/components/${r})`) + .join('\n'); + sections.push(relatedLinks); + } + + return sections.join('\n\n'); +} + +/** + * Generate markdown for a utility + */ +function generateUtilMarkdown(util: (typeof allUtils)[number]): string { + const sections: string[] = []; + + // Title and description + sections.push(`## ${util.name}`); + if (util.description) { + sections.push(util.description); + } + + // Documentation link + sections.push(`**Full Documentation:** [${util.name}](${BASE_URL}/docs/utils/${util.slug})`); + + // Load example + let exampleSource = ''; + if (util.usageExample) { + try { + const examplePath = join( + process.cwd(), + `src/examples/utils/${util.slug}/${util.usageExample}.svelte` + ); + exampleSource = readFileSync(examplePath, 'utf-8'); + exampleSource = trimCode(exampleSource); + } catch (e) { + // Example file may not exist + } + } + + if (exampleSource) { + sections.push('### Example'); + sections.push('```svelte\n' + exampleSource + '\n```'); + } + + // Related + if (util.related && util.related.length > 0) { + sections.push('### Related'); + const relatedLinks = util.related + .map((r) => `- [${r}](${BASE_URL}/docs/utils/${r})`) + .join('\n'); + sections.push(relatedLinks); + } + + return sections.join('\n\n'); +} diff --git a/docs/src/routes/docs/utils/[name]/+layout.svelte b/docs/src/routes/docs/utils/[name]/+layout.svelte index ae00dddfe..e32fd70bc 100644 --- a/docs/src/routes/docs/utils/[name]/+layout.svelte +++ b/docs/src/routes/docs/utils/[name]/+layout.svelte @@ -1,7 +1,7 @@ ') // remove data export statement + .trim(); +} + +/** + * Generate the full markdown document + */ +function generateMarkdown( + util: (typeof allUtils)[number], + exampleSource: string +): string { + const sections: string[] = []; + + // Title and description + sections.push(`# ${util.name}`); + if (util.description) { + sections.push(util.description); + } + + // Documentation link + sections.push(`**Full Documentation:** [${util.name}](https://layerchart.com/docs/utils/${util.slug})`); + + // Example + if (exampleSource) { + sections.push('## Example'); + sections.push('```svelte\n' + exampleSource + '\n```'); + } + + // Related + if (util.related && util.related.length > 0) { + sections.push('## Related'); + const relatedLinks = util.related + .map((r) => `- [${r}](https://layerchart.com/docs/utils/${r})`) + .join('\n'); + sections.push(relatedLinks); + } + + return sections.join('\n\n'); +} diff --git a/docs/src/routes/llms.txt/+server.ts b/docs/src/routes/llms.txt/+server.ts new file mode 100644 index 000000000..af7538c52 --- /dev/null +++ b/docs/src/routes/llms.txt/+server.ts @@ -0,0 +1,117 @@ +import { readdirSync } from 'fs'; +import { join } from 'path'; +import type { RequestHandler } from './$types'; +import { allComponents, allUtils } from 'content-collections'; + +const BASE_URL = 'https://layerchart.com'; + +const guides = [ + { slug: 'getting-started', name: 'Getting Started', description: 'Installation and setup guide for LayerChart' }, + { slug: 'guides/scales', name: 'Scales', description: 'Understanding scales, domains, and ranges' }, + { slug: 'guides/layers', name: 'Layers', description: 'Working with SVG, Canvas, and HTML layers' }, + { slug: 'guides/state', name: 'State', description: 'Managing chart state and reactivity' }, + { slug: 'guides/styles', name: 'Styles', description: 'Styling and theming your charts' }, + { slug: 'guides/primitives', name: 'Primitives', description: 'Low-level building blocks for custom charts' }, + { slug: 'guides/simplified-charts', name: 'Simplified Charts', description: 'Quick chart components for common use cases' }, + { slug: 'guides/features', name: 'Features', description: 'Overview of LayerChart features and capabilities' } +]; + +export const GET: RequestHandler = async () => { + const content = generateLlmsTxt(); + + return new Response(content, { + headers: { + 'Content-Type': 'text/plain; charset=utf-8' + } + }); +}; + +function generateLlmsTxt(): string { + const sections: string[] = []; + + // Header + sections.push(`# LayerChart Documentation for LLMs + +> LayerChart is a powerful, composable charting library for Svelte built on top of D3. + +This file contains links to LLM-optimized documentation in markdown format.`); + + // General section + sections.push(`## General + +${guides.map((g) => `- [${g.name}](${BASE_URL}/docs/${g.slug}): ${g.description}`).join('\n')}`); + + // Components section + const componentsList = allComponents + .filter((c) => c.slug && c.name) + .sort((a, b) => a.name.localeCompare(b.name)) + .map((c) => { + const description = c.description || `Documentation for ${c.name} component`; + return `- [${c.name}](${BASE_URL}/docs/components/${c.slug}/llms.txt): ${description}`; + }) + .join('\n'); + + sections.push(`## Components + +${componentsList}`); + + // Utilities section + const utilsList = allUtils + .filter((u) => u.slug && u.name) + .sort((a, b) => a.name.localeCompare(b.name)) + .map((u) => { + const description = u.description || `Documentation for ${u.name} utility`; + return `- [${u.name}](${BASE_URL}/docs/utils/${u.slug}/llms.txt): ${description}`; + }) + .join('\n'); + + sections.push(`## Utilities + +${utilsList}`); + + // Examples section + const examplesList = getExamplesList(); + sections.push(`## Examples + +${examplesList}`); + + return sections.join('\n\n'); +} + +/** + * Get list of all examples from the filesystem + */ +function getExamplesList(): string { + const examplesDir = join(process.cwd(), 'src/examples/components'); + const examples: string[] = []; + + try { + const componentDirs = readdirSync(examplesDir, { withFileTypes: true }) + .filter((dirent) => dirent.isDirectory()) + .map((dirent) => dirent.name) + .sort(); + + for (const componentName of componentDirs) { + const componentDir = join(examplesDir, componentName); + + try { + const exampleFiles = readdirSync(componentDir) + .filter((file) => file.endsWith('.svelte')) + .sort(); + + for (const exampleFile of exampleFiles) { + const exampleName = exampleFile.replace('.svelte', ''); + examples.push( + `- [${componentName}/${exampleName}](${BASE_URL}/docs/components/${componentName}/${exampleName}/llms.txt): Example code for ${componentName}` + ); + } + } catch (e) { + // Skip directories that can't be read + } + } + } catch (e) { + return 'Could not read examples directory.'; + } + + return examples.join('\n'); +} From 9c8a128ef279fb73d044a43097e479647a1a8537 Mon Sep 17 00:00:00 2001 From: Scott Rhamy Date: Sat, 24 Jan 2026 08:17:35 -0500 Subject: [PATCH 07/39] fix spelling error --- docs/src/routes/docs/guides/LLMs/+page.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/routes/docs/guides/LLMs/+page.md b/docs/src/routes/docs/guides/LLMs/+page.md index 923b6c114..dc591b642 100644 --- a/docs/src/routes/docs/guides/LLMs/+page.md +++ b/docs/src/routes/docs/guides/LLMs/+page.md @@ -7,7 +7,7 @@ title: LLMs import ViewSourceButton from "$lib/components/ViewSourceButton.svelte"; -The Layerchart documentation pages are designed to be accessible for humans developers using LLMs as well as large language models (LLMs) ingestesting training data. +The Layerchart documentation pages are designed to be accessible for humans developers using LLMs as well as large language models (LLMs) ingesting training data. ## :icon{name="lucide:user" class="relative -top-1"} For the Humans From eb517259421e00e6792343dbaefe0c951feded49 Mon Sep 17 00:00:00 2001 From: Scott Rhamy Date: Sat, 24 Jan 2026 08:24:11 -0500 Subject: [PATCH 08/39] another spelling fix --- docs/src/routes/docs/guides/LLMs/+page.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/routes/docs/guides/LLMs/+page.md b/docs/src/routes/docs/guides/LLMs/+page.md index dc591b642..1e7663845 100644 --- a/docs/src/routes/docs/guides/LLMs/+page.md +++ b/docs/src/routes/docs/guides/LLMs/+page.md @@ -13,7 +13,7 @@ The Layerchart documentation pages are designed to be accessible for humans deve -At the top of each documentation page, and demonstrated above, you'll find a button which copies the content of the page's documentation in Markdown to the clipboard. The convenient dropdown gives you addtional helpful options. +At the top of each documentation page, and demonstrated above, you'll find a button which copies the content of the page's documentation in Markdown to the clipboard. The convenient dropdown gives you additional helpful options. ::note The option for `View Component Source` is only shown for component pages. From 3a91975517103acfbd0d4ccecc677ef63fba7af5 Mon Sep 17 00:00:00 2001 From: Scott Rhamy Date: Mon, 26 Jan 2026 08:04:38 -0500 Subject: [PATCH 09/39] Change OpenLLMs.svelte to OpenWithButton.svelte - cleaned up logic on `ddocs/src/routes/docs/guides/[name]/llms.txt/+server.ts` was getting title by url name, now uses frontmatter like others. --- ...{OpenLLMs.svelte => OpenWithButton.svelte} | 0 .../docs/components/[name]/+layout.svelte | 5 ++-- .../components/[name]/[example]/+page.svelte | 4 ++-- .../docs/getting-started/+layout.svelte | 4 ++-- docs/src/routes/docs/guides/+layout.svelte | 6 ++--- docs/src/routes/docs/guides/LLMs/+page.md | 5 ++-- .../docs/guides/[name]/llms.txt/+server.ts | 24 +++++++++++++++---- .../routes/docs/utils/[name]/+layout.svelte | 5 ++-- 8 files changed, 32 insertions(+), 21 deletions(-) rename docs/src/lib/components/{OpenLLMs.svelte => OpenWithButton.svelte} (100%) diff --git a/docs/src/lib/components/OpenLLMs.svelte b/docs/src/lib/components/OpenWithButton.svelte similarity index 100% rename from docs/src/lib/components/OpenLLMs.svelte rename to docs/src/lib/components/OpenWithButton.svelte diff --git a/docs/src/routes/docs/components/[name]/+layout.svelte b/docs/src/routes/docs/components/[name]/+layout.svelte index 456b53b9d..e7ef7c25a 100644 --- a/docs/src/routes/docs/components/[name]/+layout.svelte +++ b/docs/src/routes/docs/components/[name]/+layout.svelte @@ -2,14 +2,13 @@ import { getSettings } from 'layerchart'; import { Button, Menu, Switch, Toggle, ToggleGroup, ToggleOption, Tooltip } from 'svelte-ux'; import { toTitleCase } from '@layerstack/utils'; - import OpenLLMsbutton from '$lib/components/OpenLLMs.svelte'; + import OpenWithButton from '$lib/components/OpenWithButton.svelte'; import { examples } from '$lib/context.js'; import { loadExample } from '$lib/examples.js'; import { page } from '$app/state'; import LucideSettings from '~icons/lucide/settings'; - import LucideCode from '~icons/lucide/code'; import LucideChevronLeft from '~icons/lucide/chevron-left'; import LucideChevronRight from '~icons/lucide/chevron-right'; @@ -129,7 +128,7 @@
{metadata.description}
- + + {#if data.metadata.title !== 'LLMs'}
- +
{/if} diff --git a/docs/src/routes/docs/guides/LLMs/+page.md b/docs/src/routes/docs/guides/LLMs/+page.md index 1e7663845..1fc3e3de5 100644 --- a/docs/src/routes/docs/guides/LLMs/+page.md +++ b/docs/src/routes/docs/guides/LLMs/+page.md @@ -3,15 +3,14 @@ title: LLMs --- The Layerchart documentation pages are designed to be accessible for humans developers using LLMs as well as large language models (LLMs) ingesting training data. ## :icon{name="lucide:user" class="relative -top-1"} For the Humans - + At the top of each documentation page, and demonstrated above, you'll find a button which copies the content of the page's documentation in Markdown to the clipboard. The convenient dropdown gives you additional helpful options. diff --git a/docs/src/routes/docs/guides/[name]/llms.txt/+server.ts b/docs/src/routes/docs/guides/[name]/llms.txt/+server.ts index ba264cc3a..ff4474ebe 100644 --- a/docs/src/routes/docs/guides/[name]/llms.txt/+server.ts +++ b/docs/src/routes/docs/guides/[name]/llms.txt/+server.ts @@ -15,13 +15,27 @@ export const GET: RequestHandler = async ({ params }) => { error(404, `Guide "${name}" not found`); } + // Extract title from frontmatter before processing + let title: string | undefined; + const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n*/); + if (frontmatterMatch) { + const frontmatter = frontmatterMatch[1]; + const titleMatch = frontmatter.match(/^title:\s*(.+)$/m); + if (titleMatch) { + title = titleMatch[1].trim().replace(/^["']|["']$/g, ''); // Remove quotes if present + } + } + + // Process content (removes frontmatter) content = processMarkdownContent(content); - // Generate title from name - const title = name - .split('-') - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' '); + // Use frontmatter title if available, otherwise generate from name + if (!title) { + title = name + .split('-') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); + } const markdown = `# ${title}\n\n${content}`; diff --git a/docs/src/routes/docs/utils/[name]/+layout.svelte b/docs/src/routes/docs/utils/[name]/+layout.svelte index e32fd70bc..9acb7e2a3 100644 --- a/docs/src/routes/docs/utils/[name]/+layout.svelte +++ b/docs/src/routes/docs/utils/[name]/+layout.svelte @@ -1,11 +1,10 @@ -

+

&]:my-5 leading-relaxed', className)} {...restProps}> {@render children?.()}

From 5bd1d0e38dfc14d6caaa87a9e64a4d6bd6e2a9a7 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sat, 31 Jan 2026 14:03:05 -0500 Subject: [PATCH 13/39] refine some wording --- docs/src/routes/docs/getting-started/+page.md | 4 ++-- docs/src/routes/docs/guides/LLMs/+page.md | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/src/routes/docs/getting-started/+page.md b/docs/src/routes/docs/getting-started/+page.md index 43ce6fae2..197e26b53 100644 --- a/docs/src/routes/docs/getting-started/+page.md +++ b/docs/src/routes/docs/getting-started/+page.md @@ -4,7 +4,7 @@ title: Getting Started LayerChart can be used standlone, or integrates nicely with other frameworks and design systems. -Provides built-in first class support for [:icon{name="logos:tailwindcss-icon" class="text-xs"} tailwindcss 4](https://tailwindcss.com/), +Provides built-in first class support for [:icon{name="logos:tailwindcss-icon" class="text-xs"} Tailwind 4](https://tailwindcss.com/), but is completely optional. The library also works seamlessly with vanilla CSS, inline styles, and [:icon{name="logos:unocss"} unoCSS](https://unocss.dev/). ::steps @@ -53,7 +53,7 @@ Use the Svelte CLI to generate a new SvelteKit project, or continue with an exis ::: ::note -To add tailwind to an existing project you can `npm sv add tailwindcss` +To add Tailwind to an existing project you can `npm sv add tailwindcss` :: ## Import `layerchart` with your package manager of choice. diff --git a/docs/src/routes/docs/guides/LLMs/+page.md b/docs/src/routes/docs/guides/LLMs/+page.md index 1fc3e3de5..614b95cb5 100644 --- a/docs/src/routes/docs/guides/LLMs/+page.md +++ b/docs/src/routes/docs/guides/LLMs/+page.md @@ -6,7 +6,7 @@ title: LLMs import OpenWithButton from "$lib/components/OpenWithButton.svelte"; -The Layerchart documentation pages are designed to be accessible for humans developers using LLMs as well as large language models (LLMs) ingesting training data. +The Layerchart documentation pages are designed to be accessible for humans developers using LLMs as well as large language models (LLMs) ingesting training data. ## :icon{name="lucide:user" class="relative -top-1"} For the Humans @@ -28,12 +28,12 @@ Not all pages may support the `/llms.txt` suffix (ie those deemed irrelevant to ## LLM-friendly Documentation 3 Ways -1. To access the LLM-friendly version of supported Layerchart documentation pages, simply append `/llms.txt` to the end of the page's URL. This will return the content in a plain-text, LLM-optimized format. This is the same text which is copied to the clipboard when you click the `View Page Markdown` button. +1. To access the LLM-friendly version of supported Layerchart documentation pages, simply append `/llms.txt` to the end of the page's URL. This will return the content in a plain-text, LLM-optimized format. This is the same text which is copied to the clipboard when you click the `Copy Page` button. :::tip -- **Standard Page**: The LineChart component documentation is available at [layerchart.com/docs/components/LineChart](/docs/components/LineChart). - -- **LLM-friendly Version**: is available at [layerchart.com/docs/components/Linechart/llms.txt](/docs/components/LineChart/llms.txt). +**Standard Page**: The LineChart component documentation is available at [/docs/components/LineChart](/docs/components/LineChart) + +**LLM-friendly Version**: is available at [/docs/components/Linechart/llms.txt](/docs/components/LineChart/llms.txt) ::: 1. To explore all supported pages in LLM-friendly format, visit the root index at [layerchart.com/llms.txt](/llms.txt). This page provides a comprehensive list of available documentation endpoints compatible with the `llms.txt` standard. From b4a658fb870b5a3aeb6e7f6ceca2df1fb638aa0a Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sat, 31 Jan 2026 17:47:25 -0500 Subject: [PATCH 14/39] Create guides content collection and use to build menu --- docs/content-collections.ts | 22 ++++++++++++++- .../LLMs/+page.md => content/guides/LLMs.md} | 1 + .../+page.md => content/guides/features.md} | 1 + .../+page.md => content/guides/layers.md} | 5 ++-- .../guides/layers/FeatureTable.svelte | 0 .../guides/layers/features.ts | 0 .../+page.md => content/guides/primitives.md} | 1 + .../+page.md => content/guides/scales.md} | 3 +- .../guides/scales/DomainRangeChart.svelte | 0 .../guides/scales/ResizableRect.svelte | 0 .../guides/simplified-charts.md} | 1 + .../+page.md => content/guides/state.md} | 1 + .../+page.md => content/guides/styles.md} | 7 +++-- .../guides/styles/color-schemes.svelte | 0 .../guides/styles/padding.svelte | 0 docs/src/lib/components/DocsMenu.svelte | 24 ++++++++-------- docs/src/lib/components/Example.svelte | 5 ++++ docs/src/lib/examples.ts | 16 +++++++---- docs/src/lib/markdown/utils.ts | 28 +++++++++++++------ docs/src/routes/docs/guides/+layout.ts | 26 ----------------- .../docs/guides/{ => [name]}/+layout.svelte | 9 ++++++ docs/src/routes/docs/guides/[name]/+layout.ts | 23 +++++++++++++++ .../routes/docs/guides/[name]/+page.svelte | 6 ++++ .../docs/guides/[name]/llms.txt/+server.ts | 2 +- docs/src/routes/docs/llms.txt/+server.ts | 18 ++++++------ docs/src/routes/llms.txt/+server.ts | 18 ++++++------ 26 files changed, 141 insertions(+), 76 deletions(-) rename docs/src/{routes/docs/guides/LLMs/+page.md => content/guides/LLMs.md} (99%) rename docs/src/{routes/docs/guides/features/+page.md => content/guides/features.md} (99%) rename docs/src/{routes/docs/guides/layers/+page.md => content/guides/layers.md} (96%) rename docs/src/{routes/docs => content}/guides/layers/FeatureTable.svelte (100%) rename docs/src/{routes/docs => content}/guides/layers/features.ts (100%) rename docs/src/{routes/docs/guides/primitives/+page.md => content/guides/primitives.md} (98%) rename docs/src/{routes/docs/guides/scales/+page.md => content/guides/scales.md} (98%) rename docs/src/{routes/docs => content}/guides/scales/DomainRangeChart.svelte (100%) rename docs/src/{routes/docs => content}/guides/scales/ResizableRect.svelte (100%) rename docs/src/{routes/docs/guides/simplified-charts/+page.md => content/guides/simplified-charts.md} (99%) rename docs/src/{routes/docs/guides/state/+page.md => content/guides/state.md} (91%) rename docs/src/{routes/docs/guides/styles/+page.md => content/guides/styles.md} (94%) rename docs/src/{routes/docs => content}/guides/styles/color-schemes.svelte (100%) rename docs/src/{routes/docs => content}/guides/styles/padding.svelte (100%) delete mode 100644 docs/src/routes/docs/guides/+layout.ts rename docs/src/routes/docs/guides/{ => [name]}/+layout.svelte (68%) create mode 100644 docs/src/routes/docs/guides/[name]/+layout.ts create mode 100644 docs/src/routes/docs/guides/[name]/+page.svelte diff --git a/docs/content-collections.ts b/docs/content-collections.ts index 829981914..938373516 100644 --- a/docs/content-collections.ts +++ b/docs/content-collections.ts @@ -152,6 +152,26 @@ const utils = defineCollection({ } }); +const guides = defineCollection({ + name: 'guides', + directory: 'src/content/guides', + include: '**/*.md', + schema: z.object({ + title: z.string(), + description: z.string().optional(), + order: z.number().optional(), + draft: z.boolean().default(false) + }), + transform: async (doc) => { + const { path } = doc._meta; + return { + ...doc, + name: doc.title, + slug: path + }; + } +}); + const releases = defineCollection({ name: 'releases', directory: 'src/content/releases', @@ -177,5 +197,5 @@ const releases = defineCollection({ }); export default defineConfig({ - collections: [components, examples, utils, releases] + collections: [components, examples, utils, guides, releases] }); diff --git a/docs/src/routes/docs/guides/LLMs/+page.md b/docs/src/content/guides/LLMs.md similarity index 99% rename from docs/src/routes/docs/guides/LLMs/+page.md rename to docs/src/content/guides/LLMs.md index 614b95cb5..e1c6f811f 100644 --- a/docs/src/routes/docs/guides/LLMs/+page.md +++ b/docs/src/content/guides/LLMs.md @@ -1,5 +1,6 @@ --- title: LLMs +order: 8 --- LayerChart provides first-class support for different types of layers including [Svg](/docs/components/Svg), [Html](/docs/components/Html), and [Canvas](/docs/components/Canvas) via [Layer](/docs/components/Layer) and [Primitive](/docs/guides/primitives) components. diff --git a/docs/src/routes/docs/guides/layers/FeatureTable.svelte b/docs/src/content/guides/layers/FeatureTable.svelte similarity index 100% rename from docs/src/routes/docs/guides/layers/FeatureTable.svelte rename to docs/src/content/guides/layers/FeatureTable.svelte diff --git a/docs/src/routes/docs/guides/layers/features.ts b/docs/src/content/guides/layers/features.ts similarity index 100% rename from docs/src/routes/docs/guides/layers/features.ts rename to docs/src/content/guides/layers/features.ts diff --git a/docs/src/routes/docs/guides/primitives/+page.md b/docs/src/content/guides/primitives.md similarity index 98% rename from docs/src/routes/docs/guides/primitives/+page.md rename to docs/src/content/guides/primitives.md index 96cee2a8f..083f08e96 100644 --- a/docs/src/routes/docs/guides/primitives/+page.md +++ b/docs/src/content/guides/primitives.md @@ -1,5 +1,6 @@ --- title: Primitives +order: 3 ---

{data.metadata.title}

diff --git a/docs/src/routes/docs/guides/[name]/+layout.ts b/docs/src/routes/docs/guides/[name]/+layout.ts new file mode 100644 index 000000000..9501a78a4 --- /dev/null +++ b/docs/src/routes/docs/guides/[name]/+layout.ts @@ -0,0 +1,23 @@ +import { getMarkdownComponent, loadExamplesFromMarkdown } from '$lib/markdown/utils.js'; + +export const load = async ({ params, parent, url }) => { + const parentData = await parent(); + + const { PageComponent, metadata } = await getMarkdownComponent(params.name, 'guides'); + + // Load any examples referenced in the markdown content + const pageExamples = await loadExamplesFromMarkdown( + metadata.content, + undefined, + 'guides', + url.pathname + ); + + const examples = { ...parentData.examples, ...pageExamples }; + + return { + PageComponent, + metadata, + examples + }; +}; diff --git a/docs/src/routes/docs/guides/[name]/+page.svelte b/docs/src/routes/docs/guides/[name]/+page.svelte new file mode 100644 index 000000000..7dc0dfc01 --- /dev/null +++ b/docs/src/routes/docs/guides/[name]/+page.svelte @@ -0,0 +1,6 @@ + + + diff --git a/docs/src/routes/docs/guides/[name]/llms.txt/+server.ts b/docs/src/routes/docs/guides/[name]/llms.txt/+server.ts index ff4474ebe..d13307a30 100644 --- a/docs/src/routes/docs/guides/[name]/llms.txt/+server.ts +++ b/docs/src/routes/docs/guides/[name]/llms.txt/+server.ts @@ -9,7 +9,7 @@ export const GET: RequestHandler = async ({ params }) => { let content = ''; try { - const mdPath = join(process.cwd(), `src/routes/docs/guides/${name}/+page.md`); + const mdPath = join(process.cwd(), `src/content/guides/${name}.md`); content = readFileSync(mdPath, 'utf-8'); } catch (e) { error(404, `Guide "${name}" not found`); diff --git a/docs/src/routes/docs/llms.txt/+server.ts b/docs/src/routes/docs/llms.txt/+server.ts index 96e27b2bf..ca11f2499 100644 --- a/docs/src/routes/docs/llms.txt/+server.ts +++ b/docs/src/routes/docs/llms.txt/+server.ts @@ -2,19 +2,21 @@ import { readFileSync } from 'fs'; import { join } from 'path'; import type { RequestHandler } from './$types'; import type { ComponentAPI } from '$lib/api-types.js'; -import { allComponents, allUtils } from 'content-collections'; +import { allComponents, allUtils, allGuides } from 'content-collections'; const BASE_URL = 'https://layerchart.com'; const guides = [ { slug: 'getting-started', name: 'Getting Started', description: 'Installation and setup guide for LayerChart' }, - { slug: 'guides/scales', name: 'Scales', description: 'Understanding scales, domains, and ranges' }, - { slug: 'guides/layers', name: 'Layers', description: 'Working with SVG, Canvas, and HTML layers' }, - { slug: 'guides/state', name: 'State', description: 'Managing chart state and reactivity' }, - { slug: 'guides/styles', name: 'Styles', description: 'Styling and theming your charts' }, - { slug: 'guides/primitives', name: 'Primitives', description: 'Low-level building blocks for custom charts' }, - { slug: 'guides/simplified-charts', name: 'Simplified Charts', description: 'Quick chart components for common use cases' }, - { slug: 'guides/features', name: 'Features', description: 'Overview of LayerChart features and capabilities' } + ...allGuides + .filter((g) => !g.draft) + .sort((a, b) => { + if (a.order !== undefined && b.order !== undefined) return a.order - b.order; + if (a.order !== undefined) return -1; + if (b.order !== undefined) return 1; + return a.name.localeCompare(b.name); + }) + .map((g) => ({ slug: `guides/${g.slug}`, name: g.name, description: g.description ?? '' })) ]; export const GET: RequestHandler = async () => { diff --git a/docs/src/routes/llms.txt/+server.ts b/docs/src/routes/llms.txt/+server.ts index af7538c52..4a4ddffd5 100644 --- a/docs/src/routes/llms.txt/+server.ts +++ b/docs/src/routes/llms.txt/+server.ts @@ -1,19 +1,21 @@ import { readdirSync } from 'fs'; import { join } from 'path'; import type { RequestHandler } from './$types'; -import { allComponents, allUtils } from 'content-collections'; +import { allComponents, allUtils, allGuides } from 'content-collections'; const BASE_URL = 'https://layerchart.com'; const guides = [ { slug: 'getting-started', name: 'Getting Started', description: 'Installation and setup guide for LayerChart' }, - { slug: 'guides/scales', name: 'Scales', description: 'Understanding scales, domains, and ranges' }, - { slug: 'guides/layers', name: 'Layers', description: 'Working with SVG, Canvas, and HTML layers' }, - { slug: 'guides/state', name: 'State', description: 'Managing chart state and reactivity' }, - { slug: 'guides/styles', name: 'Styles', description: 'Styling and theming your charts' }, - { slug: 'guides/primitives', name: 'Primitives', description: 'Low-level building blocks for custom charts' }, - { slug: 'guides/simplified-charts', name: 'Simplified Charts', description: 'Quick chart components for common use cases' }, - { slug: 'guides/features', name: 'Features', description: 'Overview of LayerChart features and capabilities' } + ...allGuides + .filter((g) => !g.draft) + .sort((a, b) => { + if (a.order !== undefined && b.order !== undefined) return a.order - b.order; + if (a.order !== undefined) return -1; + if (b.order !== undefined) return 1; + return a.name.localeCompare(b.name); + }) + .map((g) => ({ slug: `guides/${g.slug}`, name: g.name, description: g.description ?? '' })) ]; export const GET: RequestHandler = async () => { From a1edc7dbd2e7ccc99c2e6207453e7f25db206bd4 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sat, 31 Jan 2026 22:35:15 -0500 Subject: [PATCH 15/39] Extract and consolidate common llms.txt utils --- docs/src/lib/llms/utils.ts | 243 ++++++++++++++++++ .../components/[name]/[example]/+page.svelte | 1 - .../[name]/[example]/llms.txt/+server.ts | 33 +-- .../components/[name]/llms.txt/+server.ts | 142 +--------- .../docs/getting-started/llms.txt/+server.ts | 9 +- .../docs/guides/[name]/llms.txt/+server.ts | 10 +- docs/src/routes/docs/llms.txt/+server.ts | 208 +-------------- .../docs/utils/[name]/llms.txt/+server.ts | 75 +----- docs/src/routes/llms.txt/+server.ts | 26 +- 9 files changed, 275 insertions(+), 472 deletions(-) create mode 100644 docs/src/lib/llms/utils.ts diff --git a/docs/src/lib/llms/utils.ts b/docs/src/lib/llms/utils.ts new file mode 100644 index 000000000..4818f08e5 --- /dev/null +++ b/docs/src/lib/llms/utils.ts @@ -0,0 +1,243 @@ +import { readFileSync } from 'fs'; +import { join } from 'path'; +import type { ComponentAPI } from '$lib/api-types.js'; +import { allComponents, allUtils, allGuides } from 'content-collections'; + +export const BASE_URL = 'https://layerchart.com'; + +export interface GenerateMarkdownOptions { + /** Heading level for the title. 1 = "#", 2 = "##", etc. Default: 1 */ + headingLevel?: number; +} + +export interface GuideEntry { + slug: string; + name: string; + description: string; +} + +/** + * Trim code to remove module exports and data export statement + */ +export function trimCode(code: string): string { + return code + .replace(/[\s\S]*?<\/script>\n*/g, '') + .replace(/\n*\s*export \{ data \};\s*\n*\s*<\/script>/gm, '\n') + .trim(); +} + +/** + * Escape special markdown characters in table cells + */ +function escapeMarkdown(text: string): string { + return text + .replace(/\|/g, '\\|') + .replace(/\n/g, ' ') + .replace(//g, '>'); +} + +/** + * Generate markdown API table from component properties + */ +export function generateApiTable(api: ComponentAPI): string { + if (!api.properties || api.properties.length === 0) { + return ''; + } + + const rows = api.properties.map((prop) => { + const name = prop.required ? `**${prop.name}** (required)` : prop.name; + const type = `\`${escapeMarkdown(prop.type)}\``; + const defaultVal = prop.default ? `\`${escapeMarkdown(prop.default)}\`` : '-'; + const description = prop.description ? escapeMarkdown(prop.description) : '-'; + return `| ${name} | ${type} | ${defaultVal} | ${description} |`; + }); + + return `| Property | Type | Default | Description | +|----------|------|---------|-------------| +${rows.join('\n')}`; +} + +/** + * Generate markdown for a component + */ +export function generateComponentMarkdown( + component: (typeof allComponents)[number], + options: GenerateMarkdownOptions = {} +): string { + const { headingLevel = 1 } = options; + const h = (level: number) => '#'.repeat(level); + + const sections: string[] = []; + + // Title and description + sections.push(`${h(headingLevel)} ${component.name}`); + if (component.description) { + sections.push(component.description); + } + + // Metadata + if (component.category) { + sections.push(`**Category:** ${component.category}`); + } + if (component.layers && component.layers.length > 0) { + sections.push(`**Supported Layers:** ${component.layers.join(', ')}`); + } + + // Documentation link + sections.push( + `**Full Documentation:** [${component.name}](${BASE_URL}/docs/components/${component.slug})` + ); + + // Load example + let exampleSource = ''; + if (component.usageExample) { + try { + const examplePath = join( + process.cwd(), + `src/examples/components/${component.slug}/${component.usageExample}.svelte` + ); + exampleSource = readFileSync(examplePath, 'utf-8'); + exampleSource = trimCode(exampleSource); + } catch (e) { + // Example file may not exist + } + } + + if (exampleSource) { + sections.push(`${h(headingLevel + 1)} Example`); + sections.push('```svelte\n' + exampleSource + '\n```'); + } + + // Load API + let api: ComponentAPI | null = null; + try { + const apiPath = join(process.cwd(), `src/generated/api/${component.slug}.json`); + const apiContent = readFileSync(apiPath, 'utf-8'); + api = JSON.parse(apiContent); + } catch (e) { + // API file may not exist + } + + if (api) { + sections.push(`${h(headingLevel + 1)} API`); + const table = generateApiTable(api); + if (table) { + sections.push(table); + } + + if (api.extends && api.extends.length > 0) { + const extendsList = api.extends.map((e) => `\`${e.fullType}\``).join(', '); + sections.push(`**Extends:** ${extendsList}`); + } + } + + // Related + if (component.related && component.related.length > 0) { + sections.push(`${h(headingLevel + 1)} Related`); + const relatedLinks = component.related + .map((r) => `- [${r}](${BASE_URL}/docs/components/${r})`) + .join('\n'); + sections.push(relatedLinks); + } + + return sections.join('\n\n'); +} + +/** + * Generate markdown for a utility + */ +export function generateUtilMarkdown( + util: (typeof allUtils)[number], + options: GenerateMarkdownOptions = {} +): string { + const { headingLevel = 1 } = options; + const h = (level: number) => '#'.repeat(level); + + const sections: string[] = []; + + // Title and description + sections.push(`${h(headingLevel)} ${util.name}`); + if (util.description) { + sections.push(util.description); + } + + // Documentation link + sections.push( + `**Full Documentation:** [${util.name}](${BASE_URL}/docs/utils/${util.slug})` + ); + + // Load example + let exampleSource = ''; + if (util.usageExample) { + try { + const examplePath = join( + process.cwd(), + `src/examples/utils/${util.slug}/${util.usageExample}.svelte` + ); + exampleSource = readFileSync(examplePath, 'utf-8'); + exampleSource = trimCode(exampleSource); + } catch (e) { + // Example file may not exist + } + } + + if (exampleSource) { + sections.push(`${h(headingLevel + 1)} Example`); + sections.push('```svelte\n' + exampleSource + '\n```'); + } + + // Related + if (util.related && util.related.length > 0) { + sections.push(`${h(headingLevel + 1)} Related`); + const relatedLinks = util.related + .map((r) => `- [${r}](${BASE_URL}/docs/utils/${r})`) + .join('\n'); + sections.push(relatedLinks); + } + + return sections.join('\n\n'); +} + +/** + * Get sorted guides list with the getting-started entry prepended + */ +export function getSortedGuides(): GuideEntry[] { + return [ + { + slug: 'getting-started', + name: 'Getting Started', + description: 'Installation and setup guide for LayerChart' + }, + ...allGuides + .filter((g) => !g.draft) + .sort((a, b) => { + if (a.order !== undefined && b.order !== undefined) return a.order - b.order; + if (a.order !== undefined) return -1; + if (b.order !== undefined) return 1; + return a.name.localeCompare(b.name); + }) + .map((g) => ({ slug: `guides/${g.slug}`, name: g.name, description: g.description ?? '' })) + ]; +} + +/** + * Create a plain text response + */ +export function textResponse(content: string): Response { + return new Response(content, { + headers: { 'Content-Type': 'text/plain; charset=utf-8' } + }); +} + +/** + * Create a markdown response with Content-Disposition header + */ +export function markdownResponse(content: string, filename: string): Response { + return new Response(content, { + headers: { + 'Content-Type': 'text/markdown; charset=utf-8', + 'Content-Disposition': `inline; filename="${filename}"` + } + }); +} diff --git a/docs/src/routes/docs/components/[name]/[example]/+page.svelte b/docs/src/routes/docs/components/[name]/[example]/+page.svelte index d96915852..8a7b0a37f 100644 --- a/docs/src/routes/docs/components/[name]/[example]/+page.svelte +++ b/docs/src/routes/docs/components/[name]/[example]/+page.svelte @@ -15,7 +15,6 @@ let component = page.url.searchParams.get('component') ?? page.params.name!; const exampleInfo = $derived(data.catalog?.examples.find((e) => e.name === example)); - // console.log({ exampleInfo, data });
diff --git a/docs/src/routes/docs/components/[name]/[example]/llms.txt/+server.ts b/docs/src/routes/docs/components/[name]/[example]/llms.txt/+server.ts index c47a9dd7c..16ec5f7c6 100644 --- a/docs/src/routes/docs/components/[name]/[example]/llms.txt/+server.ts +++ b/docs/src/routes/docs/components/[name]/[example]/llms.txt/+server.ts @@ -4,16 +4,13 @@ import { join } from 'path'; import type { RequestHandler } from './$types'; import { allComponents } from 'content-collections'; import { processMarkdownContent } from '$lib/markdown/utils.js'; - -const BASE_URL = 'https://layerchart.com'; +import { BASE_URL, trimCode, markdownResponse } from '$lib/llms/utils.js'; export const GET: RequestHandler = async ({ params }) => { const { name, example } = params; - // Find component metadata const component = allComponents.find((c) => c.slug === name); - // Load example source code let exampleSource = ''; try { const examplePath = join(process.cwd(), `src/examples/components/${name}/${example}.svelte`); @@ -26,27 +23,9 @@ export const GET: RequestHandler = async ({ params }) => { let markdown = generateMarkdown(name, example, component, exampleSource); markdown = processMarkdownContent(markdown); - return new Response(markdown, { - headers: { - 'Content-Type': 'text/markdown; charset=utf-8', - 'Content-Disposition': `inline; filename="${name}-${example}.md"` - } - }); + return markdownResponse(markdown, `${name}-${example}.md`); }; -/** - * Trim code to remove module exports and data export statement - */ -function trimCode(code: string): string { - return code - .replace(/[\s\S]*?<\/script>\n*/g, '') - .replace(/\n*\s*export \{ data \};\s*\n*\s*<\/script>/gm, '\n') - .trim(); -} - -/** - * Generate the markdown document - */ function generateMarkdown( componentName: string, exampleName: string, @@ -55,18 +34,16 @@ function generateMarkdown( ): string { const sections: string[] = []; - // Title sections.push(`# ${componentName} - ${exampleName}`); - // Component description if available if (component?.description) { sections.push(component.description); } - // Links - sections.push(`**Component Documentation:** [${componentName}](${BASE_URL}/docs/components/${componentName})`); + sections.push( + `**Component Documentation:** [${componentName}](${BASE_URL}/docs/components/${componentName})` + ); - // Example code sections.push('## Code'); sections.push('```svelte\n' + exampleSource + '\n```'); diff --git a/docs/src/routes/docs/components/[name]/llms.txt/+server.ts b/docs/src/routes/docs/components/[name]/llms.txt/+server.ts index 2c3fc6dfc..c4b5aaa39 100644 --- a/docs/src/routes/docs/components/[name]/llms.txt/+server.ts +++ b/docs/src/routes/docs/components/[name]/llms.txt/+server.ts @@ -1,155 +1,19 @@ import { error } from '@sveltejs/kit'; -import { readFileSync } from 'fs'; -import { join } from 'path'; import type { RequestHandler } from './$types'; -import type { ComponentAPI } from '$lib/api-types.js'; import { allComponents } from 'content-collections'; import { processMarkdownContent } from '$lib/markdown/utils.js'; +import { generateComponentMarkdown, markdownResponse } from '$lib/llms/utils.js'; export const GET: RequestHandler = async ({ params }) => { const { name } = params; - // Find component metadata from content-collections const component = allComponents.find((c) => c.slug === name); if (!component) { error(404, `Component "${name}" not found`); } - // Load API JSON - let api: ComponentAPI | null = null; - try { - const apiPath = join(process.cwd(), `src/generated/api/${name}.json`); - const apiContent = readFileSync(apiPath, 'utf-8'); - api = JSON.parse(apiContent); - } catch (e) { - // API file may not exist for all components - } - - // Load first example source code - let exampleSource = ''; - if (component.usageExample) { - try { - const examplePath = join( - process.cwd(), - `src/examples/components/${name}/${component.usageExample}.svelte` - ); - exampleSource = readFileSync(examplePath, 'utf-8'); - exampleSource = trimCode(exampleSource); - } catch (e) { - // Example file may not exist - } - } - - // Build markdown content - let markdown = generateMarkdown(component, api, exampleSource); + let markdown = generateComponentMarkdown(component); markdown = processMarkdownContent(markdown); - return new Response(markdown, { - headers: { - 'Content-Type': 'text/markdown; charset=utf-8', - 'Content-Disposition': `inline; filename="${name}.md"` - } - }); + return markdownResponse(markdown, `${name}.md`); }; - -/** - * Trim code to remove module exports and data export statement - */ -function trimCode(code: string): string { - return code - .replace(/[\s\S]*?<\/script>\n*/g, '') // remove module exports section - .replace(/\n*\s*export \{ data \};\s*\n*\s*<\/script>/gm, '\n') // remove data export statement - .trim(); -} - -/** - * Generate markdown API table from component properties - */ -function generateApiTable(api: ComponentAPI): string { - if (!api.properties || api.properties.length === 0) { - return ''; - } - - const rows = api.properties.map((prop) => { - const name = prop.required ? `**${prop.name}** (required)` : prop.name; - const type = `\`${escapeMarkdown(prop.type)}\``; - const defaultVal = prop.default ? `\`${escapeMarkdown(prop.default)}\`` : '-'; - const description = prop.description ? escapeMarkdown(prop.description) : '-'; - return `| ${name} | ${type} | ${defaultVal} | ${description} |`; - }); - - return `| Property | Type | Default | Description | -|----------|------|---------|-------------| -${rows.join('\n')}`; -} - -/** - * Escape special markdown characters in table cells - */ -function escapeMarkdown(text: string): string { - return text - .replace(/\|/g, '\\|') - .replace(/\n/g, ' ') - .replace(//g, '>'); -} - -/** - * Generate the full markdown document - */ -function generateMarkdown( - component: (typeof allComponents)[number], - api: ComponentAPI | null, - exampleSource: string -): string { - const sections: string[] = []; - - // Title and description - sections.push(`# ${component.name}`); - if (component.description) { - sections.push(component.description); - } - - // Metadata - if (component.category) { - sections.push(`**Category:** ${component.category}`); - } - if (component.layers && component.layers.length > 0) { - sections.push(`**Supported Layers:** ${component.layers.join(', ')}`); - } - - // Documentation link - sections.push(`**Full Documentation:** [${component.name}](https://layerchart.com/docs/components/${component.slug})`); - - // Example (before API) - if (exampleSource) { - sections.push('## Example'); - sections.push('```svelte\n' + exampleSource + '\n```'); - } - - // API table - if (api) { - sections.push('## API'); - const table = generateApiTable(api); - if (table) { - sections.push(table); - } - - // Extended types - if (api.extends && api.extends.length > 0) { - const extendsList = api.extends.map((e) => `\`${e.fullType}\``).join(', '); - sections.push(`**Extends:** ${extendsList}`); - } - } - - // Related components - if (component.related && component.related.length > 0) { - sections.push('## Related'); - const relatedLinks = component.related - .map((r) => `- [${r}](https://layerchart.com/docs/components/${r})`) - .join('\n'); - sections.push(relatedLinks); - } - - return sections.join('\n\n'); -} diff --git a/docs/src/routes/docs/getting-started/llms.txt/+server.ts b/docs/src/routes/docs/getting-started/llms.txt/+server.ts index 71912f43d..13a9be0e3 100644 --- a/docs/src/routes/docs/getting-started/llms.txt/+server.ts +++ b/docs/src/routes/docs/getting-started/llms.txt/+server.ts @@ -2,6 +2,7 @@ import { readFileSync } from 'fs'; import { join } from 'path'; import type { RequestHandler } from './$types'; import { processMarkdownContent } from '$lib/markdown/utils.js'; +import { markdownResponse } from '$lib/llms/utils.js'; export const GET: RequestHandler = async () => { const mdPath = join(process.cwd(), 'src/routes/docs/getting-started/+page.md'); @@ -11,11 +12,5 @@ export const GET: RequestHandler = async () => { const markdown = `# Getting Started\n\n${content}`; - return new Response(markdown, { - headers: { - 'Content-Type': 'text/markdown; charset=utf-8', - 'Content-Disposition': 'inline; filename="getting-started.md"' - } - }); + return markdownResponse(markdown, 'getting-started.md'); }; - diff --git a/docs/src/routes/docs/guides/[name]/llms.txt/+server.ts b/docs/src/routes/docs/guides/[name]/llms.txt/+server.ts index d13307a30..9a8178042 100644 --- a/docs/src/routes/docs/guides/[name]/llms.txt/+server.ts +++ b/docs/src/routes/docs/guides/[name]/llms.txt/+server.ts @@ -3,6 +3,7 @@ import { readFileSync } from 'fs'; import { join } from 'path'; import type { RequestHandler } from './$types'; import { processMarkdownContent } from '$lib/markdown/utils.js'; +import { markdownResponse } from '$lib/llms/utils.js'; export const GET: RequestHandler = async ({ params }) => { const { name } = params; @@ -22,7 +23,7 @@ export const GET: RequestHandler = async ({ params }) => { const frontmatter = frontmatterMatch[1]; const titleMatch = frontmatter.match(/^title:\s*(.+)$/m); if (titleMatch) { - title = titleMatch[1].trim().replace(/^["']|["']$/g, ''); // Remove quotes if present + title = titleMatch[1].trim().replace(/^["']|["']$/g, ''); } } @@ -39,10 +40,5 @@ export const GET: RequestHandler = async ({ params }) => { const markdown = `# ${title}\n\n${content}`; - return new Response(markdown, { - headers: { - 'Content-Type': 'text/markdown; charset=utf-8', - 'Content-Disposition': `inline; filename="${name}.md"` - } - }); + return markdownResponse(markdown, `${name}.md`); }; diff --git a/docs/src/routes/docs/llms.txt/+server.ts b/docs/src/routes/docs/llms.txt/+server.ts index ca11f2499..b2da49a40 100644 --- a/docs/src/routes/docs/llms.txt/+server.ts +++ b/docs/src/routes/docs/llms.txt/+server.ts @@ -1,35 +1,20 @@ -import { readFileSync } from 'fs'; -import { join } from 'path'; import type { RequestHandler } from './$types'; -import type { ComponentAPI } from '$lib/api-types.js'; -import { allComponents, allUtils, allGuides } from 'content-collections'; - -const BASE_URL = 'https://layerchart.com'; - -const guides = [ - { slug: 'getting-started', name: 'Getting Started', description: 'Installation and setup guide for LayerChart' }, - ...allGuides - .filter((g) => !g.draft) - .sort((a, b) => { - if (a.order !== undefined && b.order !== undefined) return a.order - b.order; - if (a.order !== undefined) return -1; - if (b.order !== undefined) return 1; - return a.name.localeCompare(b.name); - }) - .map((g) => ({ slug: `guides/${g.slug}`, name: g.name, description: g.description ?? '' })) -]; +import { allComponents, allUtils } from 'content-collections'; +import { + BASE_URL, + getSortedGuides, + generateComponentMarkdown, + generateUtilMarkdown, + textResponse +} from '$lib/llms/utils.js'; export const GET: RequestHandler = async () => { const content = generateFullLlmsTxt(); - - return new Response(content, { - headers: { - 'Content-Type': 'text/plain; charset=utf-8' - } - }); + return textResponse(content); }; function generateFullLlmsTxt(): string { + const guides = getSortedGuides(); const sections: string[] = []; // Header @@ -52,8 +37,7 @@ ${guides.map((g) => `- [${g.name}](${BASE_URL}/docs/${g.slug}): ${g.description} .sort((a, b) => a.name.localeCompare(b.name)); for (const component of sortedComponents) { - const componentContent = generateComponentMarkdown(component); - sections.push(componentContent); + sections.push(generateComponentMarkdown(component, { headingLevel: 2 })); } // Utilities section - full content @@ -64,175 +48,7 @@ ${guides.map((g) => `- [${g.name}](${BASE_URL}/docs/${g.slug}): ${g.description} .sort((a, b) => a.name.localeCompare(b.name)); for (const util of sortedUtils) { - const utilContent = generateUtilMarkdown(util); - sections.push(utilContent); - } - - return sections.join('\n\n'); -} - -/** - * Trim code to remove module exports and data export statement - */ -function trimCode(code: string): string { - return code - .replace(/[\s\S]*?<\/script>\n*/g, '') - .replace(/\n*\s*export \{ data \};\s*\n*\s*<\/script>/gm, '\n') - .trim(); -} - -/** - * Escape special markdown characters in table cells - */ -function escapeMarkdown(text: string): string { - return text - .replace(/\|/g, '\\|') - .replace(/\n/g, ' ') - .replace(//g, '>'); -} - -/** - * Generate markdown API table from component properties - */ -function generateApiTable(api: ComponentAPI): string { - if (!api.properties || api.properties.length === 0) { - return ''; - } - - const rows = api.properties.map((prop) => { - const name = prop.required ? `**${prop.name}** (required)` : prop.name; - const type = `\`${escapeMarkdown(prop.type)}\``; - const defaultVal = prop.default ? `\`${escapeMarkdown(prop.default)}\`` : '-'; - const description = prop.description ? escapeMarkdown(prop.description) : '-'; - return `| ${name} | ${type} | ${defaultVal} | ${description} |`; - }); - - return `| Property | Type | Default | Description | -|----------|------|---------|-------------| -${rows.join('\n')}`; -} - -/** - * Generate markdown for a component - */ -function generateComponentMarkdown(component: (typeof allComponents)[number]): string { - const sections: string[] = []; - - // Title and description - sections.push(`## ${component.name}`); - if (component.description) { - sections.push(component.description); - } - - // Metadata - if (component.category) { - sections.push(`**Category:** ${component.category}`); - } - if (component.layers && component.layers.length > 0) { - sections.push(`**Supported Layers:** ${component.layers.join(', ')}`); - } - - // Documentation link - sections.push(`**Full Documentation:** [${component.name}](${BASE_URL}/docs/components/${component.slug})`); - - // Load example - let exampleSource = ''; - if (component.usageExample) { - try { - const examplePath = join( - process.cwd(), - `src/examples/components/${component.slug}/${component.usageExample}.svelte` - ); - exampleSource = readFileSync(examplePath, 'utf-8'); - exampleSource = trimCode(exampleSource); - } catch (e) { - // Example file may not exist - } - } - - if (exampleSource) { - sections.push('### Example'); - sections.push('```svelte\n' + exampleSource + '\n```'); - } - - // Load API - let api: ComponentAPI | null = null; - try { - const apiPath = join(process.cwd(), `src/generated/api/${component.slug}.json`); - const apiContent = readFileSync(apiPath, 'utf-8'); - api = JSON.parse(apiContent); - } catch (e) { - // API file may not exist - } - - if (api) { - sections.push('### API'); - const table = generateApiTable(api); - if (table) { - sections.push(table); - } - - if (api.extends && api.extends.length > 0) { - const extendsList = api.extends.map((e) => `\`${e.fullType}\``).join(', '); - sections.push(`**Extends:** ${extendsList}`); - } - } - - // Related - if (component.related && component.related.length > 0) { - sections.push('### Related'); - const relatedLinks = component.related - .map((r) => `- [${r}](${BASE_URL}/docs/components/${r})`) - .join('\n'); - sections.push(relatedLinks); - } - - return sections.join('\n\n'); -} - -/** - * Generate markdown for a utility - */ -function generateUtilMarkdown(util: (typeof allUtils)[number]): string { - const sections: string[] = []; - - // Title and description - sections.push(`## ${util.name}`); - if (util.description) { - sections.push(util.description); - } - - // Documentation link - sections.push(`**Full Documentation:** [${util.name}](${BASE_URL}/docs/utils/${util.slug})`); - - // Load example - let exampleSource = ''; - if (util.usageExample) { - try { - const examplePath = join( - process.cwd(), - `src/examples/utils/${util.slug}/${util.usageExample}.svelte` - ); - exampleSource = readFileSync(examplePath, 'utf-8'); - exampleSource = trimCode(exampleSource); - } catch (e) { - // Example file may not exist - } - } - - if (exampleSource) { - sections.push('### Example'); - sections.push('```svelte\n' + exampleSource + '\n```'); - } - - // Related - if (util.related && util.related.length > 0) { - sections.push('### Related'); - const relatedLinks = util.related - .map((r) => `- [${r}](${BASE_URL}/docs/utils/${r})`) - .join('\n'); - sections.push(relatedLinks); + sections.push(generateUtilMarkdown(util, { headingLevel: 2 })); } return sections.join('\n\n'); diff --git a/docs/src/routes/docs/utils/[name]/llms.txt/+server.ts b/docs/src/routes/docs/utils/[name]/llms.txt/+server.ts index dd805c8db..def2373a1 100644 --- a/docs/src/routes/docs/utils/[name]/llms.txt/+server.ts +++ b/docs/src/routes/docs/utils/[name]/llms.txt/+server.ts @@ -1,88 +1,19 @@ import { error } from '@sveltejs/kit'; -import { readFileSync } from 'fs'; -import { join } from 'path'; import type { RequestHandler } from './$types'; import { allUtils } from 'content-collections'; import { processMarkdownContent } from '$lib/markdown/utils.js'; +import { generateUtilMarkdown, markdownResponse } from '$lib/llms/utils.js'; export const GET: RequestHandler = async ({ params }) => { const { name } = params; - // Find util metadata from content-collections const util = allUtils.find((u) => u.slug === name); if (!util) { error(404, `Utility "${name}" not found`); } - // Load first example source code - let exampleSource = ''; - if (util.usageExample) { - try { - const examplePath = join( - process.cwd(), - `src/examples/utils/${name}/${util.usageExample}.svelte` - ); - exampleSource = readFileSync(examplePath, 'utf-8'); - exampleSource = trimCode(exampleSource); - } catch (e) { - // Example file may not exist - } - } - - // Build markdown content - let markdown = generateMarkdown(util, exampleSource); + let markdown = generateUtilMarkdown(util); markdown = processMarkdownContent(markdown); - return new Response(markdown, { - headers: { - 'Content-Type': 'text/markdown; charset=utf-8', - 'Content-Disposition': `inline; filename="${name}.md"` - } - }); + return markdownResponse(markdown, `${name}.md`); }; - -/** - * Trim code to remove module exports and data export statement - */ -function trimCode(code: string): string { - return code - .replace(/[\s\S]*?<\/script>\n*/g, '') // remove module exports section - .replace(/\n*\s*export \{ data \};\s*\n*\s*<\/script>/gm, '\n') // remove data export statement - .trim(); -} - -/** - * Generate the full markdown document - */ -function generateMarkdown( - util: (typeof allUtils)[number], - exampleSource: string -): string { - const sections: string[] = []; - - // Title and description - sections.push(`# ${util.name}`); - if (util.description) { - sections.push(util.description); - } - - // Documentation link - sections.push(`**Full Documentation:** [${util.name}](https://layerchart.com/docs/utils/${util.slug})`); - - // Example - if (exampleSource) { - sections.push('## Example'); - sections.push('```svelte\n' + exampleSource + '\n```'); - } - - // Related - if (util.related && util.related.length > 0) { - sections.push('## Related'); - const relatedLinks = util.related - .map((r) => `- [${r}](https://layerchart.com/docs/utils/${r})`) - .join('\n'); - sections.push(relatedLinks); - } - - return sections.join('\n\n'); -} diff --git a/docs/src/routes/llms.txt/+server.ts b/docs/src/routes/llms.txt/+server.ts index 4a4ddffd5..ab49b83fb 100644 --- a/docs/src/routes/llms.txt/+server.ts +++ b/docs/src/routes/llms.txt/+server.ts @@ -1,34 +1,16 @@ import { readdirSync } from 'fs'; import { join } from 'path'; import type { RequestHandler } from './$types'; -import { allComponents, allUtils, allGuides } from 'content-collections'; - -const BASE_URL = 'https://layerchart.com'; - -const guides = [ - { slug: 'getting-started', name: 'Getting Started', description: 'Installation and setup guide for LayerChart' }, - ...allGuides - .filter((g) => !g.draft) - .sort((a, b) => { - if (a.order !== undefined && b.order !== undefined) return a.order - b.order; - if (a.order !== undefined) return -1; - if (b.order !== undefined) return 1; - return a.name.localeCompare(b.name); - }) - .map((g) => ({ slug: `guides/${g.slug}`, name: g.name, description: g.description ?? '' })) -]; +import { allComponents, allUtils } from 'content-collections'; +import { BASE_URL, getSortedGuides, textResponse } from '$lib/llms/utils.js'; export const GET: RequestHandler = async () => { const content = generateLlmsTxt(); - - return new Response(content, { - headers: { - 'Content-Type': 'text/plain; charset=utf-8' - } - }); + return textResponse(content); }; function generateLlmsTxt(): string { + const guides = getSortedGuides(); const sections: string[] = []; // Header From 8852b77ed1883aa1c57ae943a541ee8645e83286 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sat, 31 Jan 2026 22:45:34 -0500 Subject: [PATCH 16/39] Add common sortCollection util --- docs/src/lib/collections.ts | 12 ++++++++++++ docs/src/lib/components/DocsMenu.svelte | 24 +++--------------------- docs/src/lib/llms/utils.ts | 10 ++-------- docs/src/routes/llms.txt/+server.ts | 3 ++- 4 files changed, 19 insertions(+), 30 deletions(-) create mode 100644 docs/src/lib/collections.ts diff --git a/docs/src/lib/collections.ts b/docs/src/lib/collections.ts new file mode 100644 index 000000000..22aa9ca37 --- /dev/null +++ b/docs/src/lib/collections.ts @@ -0,0 +1,12 @@ +/** + * Sort a collection by optional `order` field first (items with order come before those without), + * then alphabetically by `name`. + */ +export function sortCollection(items: T[]): T[] { + return items.slice().sort((a, b) => { + if (a.order !== undefined && b.order !== undefined) return a.order - b.order; + if (a.order !== undefined) return -1; + if (b.order !== undefined) return 1; + return a.name.localeCompare(b.name); + }); +} diff --git a/docs/src/lib/components/DocsMenu.svelte b/docs/src/lib/components/DocsMenu.svelte index e3885fe34..bf5aacbff 100644 --- a/docs/src/lib/components/DocsMenu.svelte +++ b/docs/src/lib/components/DocsMenu.svelte @@ -3,6 +3,7 @@ import { flatGroup } from 'd3-array'; import { allComponents, allUtils, allGuides } from 'content-collections'; + import { sortCollection } from '$lib/collections'; import { page } from '$app/state'; import { sortFunc } from '@layerstack/utils'; import { cls } from '@layerstack/tailwind'; @@ -22,16 +23,7 @@ let { onItemClick, class: className }: { onItemClick?: () => void; class?: string } = $props(); - const guides = allGuides - .filter((g) => !g.draft) - .sort((a, b) => { - if (a.order !== undefined && b.order !== undefined) { - return a.order - b.order; - } - if (a.order !== undefined) return -1; - if (b.order !== undefined) return 1; - return a.name.localeCompare(b.name); - }); + const guides = sortCollection(allGuides.filter((g) => !g.draft)); const componentsByCategory = flatGroup(allComponents, (d) => d.category?.toLowerCase()) .filter(([category]) => category !== 'examples') @@ -99,17 +91,7 @@

{category}

- {#each components.sort((a, b) => { - // If both have order, sort by order - if (a.order !== undefined && b.order !== undefined) { - return a.order - b.order; - } - // Items with order come first - if (a.order !== undefined) return -1; - if (b.order !== undefined) return 1; - // Both without order, sort alphabetically by name - return a.name.localeCompare(b.name); - }) as component} + {#each sortCollection(components) as component} {@render navItem({ label: component.name, path: `/docs/components/${component.slug}` })} {/each}
diff --git a/docs/src/lib/llms/utils.ts b/docs/src/lib/llms/utils.ts index 4818f08e5..17bd9deed 100644 --- a/docs/src/lib/llms/utils.ts +++ b/docs/src/lib/llms/utils.ts @@ -2,6 +2,7 @@ import { readFileSync } from 'fs'; import { join } from 'path'; import type { ComponentAPI } from '$lib/api-types.js'; import { allComponents, allUtils, allGuides } from 'content-collections'; +import { sortCollection } from '$lib/collections.js'; export const BASE_URL = 'https://layerchart.com'; @@ -209,14 +210,7 @@ export function getSortedGuides(): GuideEntry[] { name: 'Getting Started', description: 'Installation and setup guide for LayerChart' }, - ...allGuides - .filter((g) => !g.draft) - .sort((a, b) => { - if (a.order !== undefined && b.order !== undefined) return a.order - b.order; - if (a.order !== undefined) return -1; - if (b.order !== undefined) return 1; - return a.name.localeCompare(b.name); - }) + ...sortCollection(allGuides.filter((g) => !g.draft)) .map((g) => ({ slug: `guides/${g.slug}`, name: g.name, description: g.description ?? '' })) ]; } diff --git a/docs/src/routes/llms.txt/+server.ts b/docs/src/routes/llms.txt/+server.ts index ab49b83fb..3e089fcc7 100644 --- a/docs/src/routes/llms.txt/+server.ts +++ b/docs/src/routes/llms.txt/+server.ts @@ -1,7 +1,8 @@ +import { allComponents, allUtils } from 'content-collections'; import { readdirSync } from 'fs'; import { join } from 'path'; + import type { RequestHandler } from './$types'; -import { allComponents, allUtils } from 'content-collections'; import { BASE_URL, getSortedGuides, textResponse } from '$lib/llms/utils.js'; export const GET: RequestHandler = async () => { From df0b8cb0111b77d794f77b8cabfaccc6881a6ba6 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sat, 31 Jan 2026 22:55:52 -0500 Subject: [PATCH 17/39] add component links to examples llms.txt --- .../[name]/[example]/llms.txt/+server.ts | 33 +++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/docs/src/routes/docs/components/[name]/[example]/llms.txt/+server.ts b/docs/src/routes/docs/components/[name]/[example]/llms.txt/+server.ts index 16ec5f7c6..b99d7475f 100644 --- a/docs/src/routes/docs/components/[name]/[example]/llms.txt/+server.ts +++ b/docs/src/routes/docs/components/[name]/[example]/llms.txt/+server.ts @@ -3,6 +3,7 @@ import { readFileSync } from 'fs'; import { join } from 'path'; import type { RequestHandler } from './$types'; import { allComponents } from 'content-collections'; +import type { ComponentCatalog } from '$examples/catalog/types.js'; import { processMarkdownContent } from '$lib/markdown/utils.js'; import { BASE_URL, trimCode, markdownResponse } from '$lib/llms/utils.js'; @@ -20,7 +21,22 @@ export const GET: RequestHandler = async ({ params }) => { error(404, `Example "${example}" not found for component "${name}"`); } - let markdown = generateMarkdown(name, example, component, exampleSource); + // Load catalog to find components used in this example + let catalog: ComponentCatalog | null = null; + try { + const catalogPath = join(process.cwd(), `src/examples/catalog/${name}.json`); + catalog = JSON.parse(readFileSync(catalogPath, 'utf-8')); + } catch (e) { + // Catalog may not exist + } + + const exampleInfo = catalog?.examples.find((e) => e.name === example); + const usedComponentNames = [...new Set(exampleInfo?.components.map((c) => c.component) ?? [])]; + const usedComponents = usedComponentNames + .map((c) => allComponents.find((ac) => ac.name === c)) + .filter((c) => c != null); + + let markdown = generateMarkdown(name, example, component, exampleSource, usedComponents); markdown = processMarkdownContent(markdown); return markdownResponse(markdown, `${name}-${example}.md`); @@ -30,7 +46,8 @@ function generateMarkdown( componentName: string, exampleName: string, component: (typeof allComponents)[number] | undefined, - exampleSource: string + exampleSource: string, + usedComponents: (typeof allComponents)[number][] ): string { const sections: string[] = []; @@ -40,12 +57,16 @@ function generateMarkdown( sections.push(component.description); } - sections.push( - `**Component Documentation:** [${componentName}](${BASE_URL}/docs/components/${componentName})` - ); - sections.push('## Code'); sections.push('```svelte\n' + exampleSource + '\n```'); + if (usedComponents.length > 0) { + sections.push('## Component Docs'); + const links = usedComponents + .map((comp) => `- [${comp.name}](${BASE_URL}/docs/components/${comp.slug}/llms.txt)`) + .join('\n'); + sections.push(links); + } + return sections.join('\n\n'); } From f48bbd7b5132bba9110ef18e730788d3bc459acf Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sat, 31 Jan 2026 23:26:04 -0500 Subject: [PATCH 18/39] consolidate more logic into llms/utils.ts --- docs/src/lib/llms/utils.ts | 115 +++++++++++++++--- docs/src/lib/markdown/utils.ts | 32 +++-- .../[name]/[example]/llms.txt/+server.ts | 4 +- .../docs/getting-started/llms.txt/+server.ts | 12 +- .../docs/guides/[name]/llms.txt/+server.ts | 34 +----- docs/src/routes/docs/llms.txt/+server.ts | 9 +- docs/src/routes/llms.txt/+server.ts | 49 ++------ 7 files changed, 142 insertions(+), 113 deletions(-) diff --git a/docs/src/lib/llms/utils.ts b/docs/src/lib/llms/utils.ts index 17bd9deed..378d48fbd 100644 --- a/docs/src/lib/llms/utils.ts +++ b/docs/src/lib/llms/utils.ts @@ -3,9 +3,23 @@ import { join } from 'path'; import type { ComponentAPI } from '$lib/api-types.js'; import { allComponents, allUtils, allGuides } from 'content-collections'; import { sortCollection } from '$lib/collections.js'; +import { processMarkdownContent } from '$lib/markdown/utils.js'; export const BASE_URL = 'https://layerchart.com'; +/** Generate URL for a docs page */ +export function docsUrl(type: 'components' | 'utils' | 'guides', slug: string): string { + if (type === 'guides') { + return `${BASE_URL}/docs/${slug}`; + } + return `${BASE_URL}/docs/${type}/${slug}`; +} + +/** Generate URL for an llms.txt endpoint */ +export function llmsUrl(type: 'components' | 'utils' | 'guides', slug: string): string { + return `${docsUrl(type, slug)}/llms.txt`; +} + export interface GenerateMarkdownOptions { /** Heading level for the title. 1 = "#", 2 = "##", etc. Default: 1 */ headingLevel?: number; @@ -31,11 +45,7 @@ export function trimCode(code: string): string { * Escape special markdown characters in table cells */ function escapeMarkdown(text: string): string { - return text - .replace(/\|/g, '\\|') - .replace(/\n/g, ' ') - .replace(//g, '>'); + return text.replace(/\|/g, '\\|').replace(/\n/g, ' ').replace(//g, '>'); } /** @@ -87,7 +97,7 @@ export function generateComponentMarkdown( // Documentation link sections.push( - `**Full Documentation:** [${component.name}](${BASE_URL}/docs/components/${component.slug})` + `**Full Documentation:** [${component.name}](${docsUrl('components', component.slug)})` ); // Load example @@ -137,7 +147,7 @@ export function generateComponentMarkdown( if (component.related && component.related.length > 0) { sections.push(`${h(headingLevel + 1)} Related`); const relatedLinks = component.related - .map((r) => `- [${r}](${BASE_URL}/docs/components/${r})`) + .map((r) => `- [${r}](${docsUrl('components', r)})`) .join('\n'); sections.push(relatedLinks); } @@ -164,9 +174,7 @@ export function generateUtilMarkdown( } // Documentation link - sections.push( - `**Full Documentation:** [${util.name}](${BASE_URL}/docs/utils/${util.slug})` - ); + sections.push(`**Full Documentation:** [${util.name}](${docsUrl('utils', util.slug)})`); // Load example let exampleSource = ''; @@ -191,9 +199,7 @@ export function generateUtilMarkdown( // Related if (util.related && util.related.length > 0) { sections.push(`${h(headingLevel + 1)} Related`); - const relatedLinks = util.related - .map((r) => `- [${r}](${BASE_URL}/docs/utils/${r})`) - .join('\n'); + const relatedLinks = util.related.map((r) => `- [${r}](${docsUrl('utils', r)})`).join('\n'); sections.push(relatedLinks); } @@ -210,11 +216,88 @@ export function getSortedGuides(): GuideEntry[] { name: 'Getting Started', description: 'Installation and setup guide for LayerChart' }, - ...sortCollection(allGuides.filter((g) => !g.draft)) - .map((g) => ({ slug: `guides/${g.slug}`, name: g.name, description: g.description ?? '' })) + ...sortCollection(allGuides.filter((g) => !g.draft)).map((g) => ({ + slug: `guides/${g.slug}`, + name: g.name, + description: g.description ?? '' + })) ]; } +/** + * Generate the "General" section listing all guides as links. + */ +export function generateGuidesSection(): string { + const guides = getSortedGuides(); + return `## General\n\n${guides.map((g) => `- [${g.name}](${docsUrl('guides', g.slug)}): ${g.description}`).join('\n')}`; +} + +export interface GenerateGuideMarkdownOptions { + /** The name/slug of the guide (e.g., 'getting-started', 'styles') */ + name: string; + /** The filesystem path to the markdown file */ + filePath: string; + /** Optional explicit title. If not provided, title is extracted from frontmatter, + * falling back to title-casing the name. */ + title?: string; +} + +/** + * Read a guide markdown file, extract its title, process the content, + * and return the final markdown string. + */ +export function generateGuideMarkdown(options: GenerateGuideMarkdownOptions): string { + const { name, filePath, title: explicitTitle } = options; + + const raw = readFileSync(filePath, 'utf-8'); + + // Extract title from frontmatter if not explicitly provided + let title = explicitTitle; + if (!title) { + const frontmatterMatch = raw.match(/^---\n([\s\S]*?)\n---\n*/); + if (frontmatterMatch) { + const titleMatch = frontmatterMatch[1].match(/^title:\s*(.+)$/m); + if (titleMatch) { + title = titleMatch[1].trim().replace(/^["']|["']$/g, ''); + } + } + } + if (!title) { + title = name + .split('-') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); + } + + const content = processMarkdownContent(raw); + return `# ${title}\n\n${content}`; +} + +export interface CollectionListOptions { + title: string; + items: Array<{ slug: string; name: string; description?: string }>; + type: 'components' | 'utils'; +} + +/** + * Generate a markdown section listing collection items as links to their llms.txt endpoints. + */ +export function generateCollectionListSection(options: CollectionListOptions): string { + const { title, items, type } = options; + + const lines = items + .filter((item) => item.slug && item.name) + .sort((a, b) => a.name.localeCompare(b.name)) + .map((item) => { + const description = + item.description || + `Documentation for ${item.name} ${type === 'components' ? 'component' : 'utility'}`; + return `- [${item.name}](${llmsUrl(type, item.slug)}): ${description}`; + }); + + return `## ${title}\n\n${lines.join('\n')}`; +} + /** * Create a plain text response */ @@ -225,7 +308,7 @@ export function textResponse(content: string): Response { } /** - * Create a markdown response with Content-Disposition header + * Create a markdown response */ export function markdownResponse(content: string, filename: string): Response { return new Response(content, { diff --git a/docs/src/lib/markdown/utils.ts b/docs/src/lib/markdown/utils.ts index bac7da684..ef66bea4a 100644 --- a/docs/src/lib/markdown/utils.ts +++ b/docs/src/lib/markdown/utils.ts @@ -138,7 +138,11 @@ export async function loadExamplesFromMarkdown( // Load component-based examples in parallel await Promise.all( componentExamples.map(async (ex) => { - const loaded = await loadExample(ex.component, ex.name, type === 'guides' ? 'components' : type); + const loaded = await loadExample( + ex.component, + ex.name, + type === 'guides' ? 'components' : type + ); if (loaded) { if (!examples[ex.component]) { examples[ex.component] = {}; @@ -183,7 +187,9 @@ function processOutsideCodeBlocks(content: string, processor: (text: string) => .join(''); } -// Process markdown content for LLMs by removing custom syntax and converting to vanilla markdown +/** + * Process markdown content for LLMs by removing custom syntax and converting to vanilla markdown + */ export function processMarkdownContent(content: string): string { // Remove frontmatter (YAML between --- markers at start of file) content = content.replace(/^---\n[\s\S]*?\n---\n*/, ''); @@ -212,7 +218,8 @@ export function processMarkdownContent(content: string): string { /:::tabs\{key="([^"]+)"\}\s*([\s\S]*?)(?=\n:::(?:\s*$|\s*\n))\n:::/gm, (_, key, tabsContent) => { const tabs: { label: string; content: string }[] = []; - const tabRegex = /::tab\{label="([^"]+)"[^}]*\}\s*([\s\S]*?)\s*(?=\n\s*::(?:\s*$|\s+))\n\s*::/gm; + const tabRegex = + /::tab\{label="([^"]+)"[^}]*\}\s*([\s\S]*?)\s*(?=\n\s*::(?:\s*$|\s+))\n\s*::/gm; let match; while ((match = tabRegex.exec(tabsContent)) !== null) { tabs.push({ label: match[1], content: match[2].trim() }); @@ -245,13 +252,16 @@ export function processMarkdownContent(content: string): string { ); // Convert ::steps to numbered list (convert ## headings to numbered items) - content = content.replace(/::steps\s*([\s\S]*?)(?=\n::(?:\s*$|\s*\n))\n::/gm, (_, stepsContent: string) => { - let stepNum = 0; - return stepsContent.replace(/^## (.+)$/gm, (_match: string, heading: string) => { - stepNum++; - return `**${stepNum}. ${heading}**`; - }); - }); + content = content.replace( + /::steps\s*([\s\S]*?)(?=\n::(?:\s*$|\s*\n))\n::/gm, + (_, stepsContent: string) => { + let stepNum = 0; + return stepsContent.replace(/^## (.+)$/gm, (_match: string, heading: string) => { + stepNum++; + return `**${stepNum}. ${heading}**`; + }); + } + ); // Remove any remaining standalone :: content = content.replace(/^::\s*$/gm, ''); @@ -270,4 +280,4 @@ export function processMarkdownContent(content: string): string { content = content.replace(/\n{3,}/g, '\n\n'); return content.trim(); -} \ No newline at end of file +} diff --git a/docs/src/routes/docs/components/[name]/[example]/llms.txt/+server.ts b/docs/src/routes/docs/components/[name]/[example]/llms.txt/+server.ts index b99d7475f..1efea694e 100644 --- a/docs/src/routes/docs/components/[name]/[example]/llms.txt/+server.ts +++ b/docs/src/routes/docs/components/[name]/[example]/llms.txt/+server.ts @@ -5,7 +5,7 @@ import type { RequestHandler } from './$types'; import { allComponents } from 'content-collections'; import type { ComponentCatalog } from '$examples/catalog/types.js'; import { processMarkdownContent } from '$lib/markdown/utils.js'; -import { BASE_URL, trimCode, markdownResponse } from '$lib/llms/utils.js'; +import { llmsUrl, trimCode, markdownResponse } from '$lib/llms/utils.js'; export const GET: RequestHandler = async ({ params }) => { const { name, example } = params; @@ -63,7 +63,7 @@ function generateMarkdown( if (usedComponents.length > 0) { sections.push('## Component Docs'); const links = usedComponents - .map((comp) => `- [${comp.name}](${BASE_URL}/docs/components/${comp.slug}/llms.txt)`) + .map((comp) => `- [${comp.name}](${llmsUrl('components', comp.slug)})`) .join('\n'); sections.push(links); } diff --git a/docs/src/routes/docs/getting-started/llms.txt/+server.ts b/docs/src/routes/docs/getting-started/llms.txt/+server.ts index 13a9be0e3..30a4bb271 100644 --- a/docs/src/routes/docs/getting-started/llms.txt/+server.ts +++ b/docs/src/routes/docs/getting-started/llms.txt/+server.ts @@ -1,16 +1,10 @@ -import { readFileSync } from 'fs'; import { join } from 'path'; import type { RequestHandler } from './$types'; -import { processMarkdownContent } from '$lib/markdown/utils.js'; -import { markdownResponse } from '$lib/llms/utils.js'; +import { generateGuideMarkdown, markdownResponse } from '$lib/llms/utils.js'; export const GET: RequestHandler = async () => { - const mdPath = join(process.cwd(), 'src/routes/docs/getting-started/+page.md'); - let content = readFileSync(mdPath, 'utf-8'); - - content = processMarkdownContent(content); - - const markdown = `# Getting Started\n\n${content}`; + const filePath = join(process.cwd(), 'src/routes/docs/getting-started/+page.md'); + const markdown = generateGuideMarkdown({ name: 'getting-started', filePath, title: 'Getting Started' }); return markdownResponse(markdown, 'getting-started.md'); }; diff --git a/docs/src/routes/docs/guides/[name]/llms.txt/+server.ts b/docs/src/routes/docs/guides/[name]/llms.txt/+server.ts index 9a8178042..7c220f269 100644 --- a/docs/src/routes/docs/guides/[name]/llms.txt/+server.ts +++ b/docs/src/routes/docs/guides/[name]/llms.txt/+server.ts @@ -1,44 +1,18 @@ import { error } from '@sveltejs/kit'; -import { readFileSync } from 'fs'; import { join } from 'path'; import type { RequestHandler } from './$types'; -import { processMarkdownContent } from '$lib/markdown/utils.js'; -import { markdownResponse } from '$lib/llms/utils.js'; +import { generateGuideMarkdown, markdownResponse } from '$lib/llms/utils.js'; export const GET: RequestHandler = async ({ params }) => { const { name } = params; - let content = ''; + let markdown: string; try { - const mdPath = join(process.cwd(), `src/content/guides/${name}.md`); - content = readFileSync(mdPath, 'utf-8'); + const filePath = join(process.cwd(), `src/content/guides/${name}.md`); + markdown = generateGuideMarkdown({ name, filePath }); } catch (e) { error(404, `Guide "${name}" not found`); } - // Extract title from frontmatter before processing - let title: string | undefined; - const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n*/); - if (frontmatterMatch) { - const frontmatter = frontmatterMatch[1]; - const titleMatch = frontmatter.match(/^title:\s*(.+)$/m); - if (titleMatch) { - title = titleMatch[1].trim().replace(/^["']|["']$/g, ''); - } - } - - // Process content (removes frontmatter) - content = processMarkdownContent(content); - - // Use frontmatter title if available, otherwise generate from name - if (!title) { - title = name - .split('-') - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' '); - } - - const markdown = `# ${title}\n\n${content}`; - return markdownResponse(markdown, `${name}.md`); }; diff --git a/docs/src/routes/docs/llms.txt/+server.ts b/docs/src/routes/docs/llms.txt/+server.ts index b2da49a40..a5185ecf6 100644 --- a/docs/src/routes/docs/llms.txt/+server.ts +++ b/docs/src/routes/docs/llms.txt/+server.ts @@ -1,8 +1,7 @@ import type { RequestHandler } from './$types'; import { allComponents, allUtils } from 'content-collections'; import { - BASE_URL, - getSortedGuides, + generateGuidesSection, generateComponentMarkdown, generateUtilMarkdown, textResponse @@ -14,7 +13,6 @@ export const GET: RequestHandler = async () => { }; function generateFullLlmsTxt(): string { - const guides = getSortedGuides(); const sections: string[] = []; // Header @@ -24,10 +22,7 @@ function generateFullLlmsTxt(): string { This file contains the complete LLM-optimized documentation for all components and utilities.`); - // General section (links only, as these are markdown pages) - sections.push(`## General - -${guides.map((g) => `- [${g.name}](${BASE_URL}/docs/${g.slug}): ${g.description}`).join('\n')}`); + sections.push(generateGuidesSection()); // Components section - full content sections.push('---\n\n# Components'); diff --git a/docs/src/routes/llms.txt/+server.ts b/docs/src/routes/llms.txt/+server.ts index 3e089fcc7..378bd280c 100644 --- a/docs/src/routes/llms.txt/+server.ts +++ b/docs/src/routes/llms.txt/+server.ts @@ -3,7 +3,12 @@ import { readdirSync } from 'fs'; import { join } from 'path'; import type { RequestHandler } from './$types'; -import { BASE_URL, getSortedGuides, textResponse } from '$lib/llms/utils.js'; +import { + generateGuidesSection, + generateCollectionListSection, + llmsUrl, + textResponse +} from '$lib/llms/utils.js'; export const GET: RequestHandler = async () => { const content = generateLlmsTxt(); @@ -11,7 +16,6 @@ export const GET: RequestHandler = async () => { }; function generateLlmsTxt(): string { - const guides = getSortedGuides(); const sections: string[] = []; // Header @@ -21,44 +25,13 @@ function generateLlmsTxt(): string { This file contains links to LLM-optimized documentation in markdown format.`); - // General section - sections.push(`## General - -${guides.map((g) => `- [${g.name}](${BASE_URL}/docs/${g.slug}): ${g.description}`).join('\n')}`); - - // Components section - const componentsList = allComponents - .filter((c) => c.slug && c.name) - .sort((a, b) => a.name.localeCompare(b.name)) - .map((c) => { - const description = c.description || `Documentation for ${c.name} component`; - return `- [${c.name}](${BASE_URL}/docs/components/${c.slug}/llms.txt): ${description}`; - }) - .join('\n'); - - sections.push(`## Components - -${componentsList}`); - - // Utilities section - const utilsList = allUtils - .filter((u) => u.slug && u.name) - .sort((a, b) => a.name.localeCompare(b.name)) - .map((u) => { - const description = u.description || `Documentation for ${u.name} utility`; - return `- [${u.name}](${BASE_URL}/docs/utils/${u.slug}/llms.txt): ${description}`; - }) - .join('\n'); - - sections.push(`## Utilities - -${utilsList}`); + sections.push(generateGuidesSection()); + sections.push(generateCollectionListSection({ title: 'Components', items: allComponents, type: 'components' })); + sections.push(generateCollectionListSection({ title: 'Utilities', items: allUtils, type: 'utils' })); // Examples section const examplesList = getExamplesList(); - sections.push(`## Examples - -${examplesList}`); + sections.push(`## Examples\n\n${examplesList}`); return sections.join('\n\n'); } @@ -87,7 +60,7 @@ function getExamplesList(): string { for (const exampleFile of exampleFiles) { const exampleName = exampleFile.replace('.svelte', ''); examples.push( - `- [${componentName}/${exampleName}](${BASE_URL}/docs/components/${componentName}/${exampleName}/llms.txt): Example code for ${componentName}` + `- [${componentName}/${exampleName}](${llmsUrl('components', `${componentName}/${exampleName}`)}): Example code for ${componentName}` ); } } catch (e) { From 1e27cc07f67aedf5514518433194fa5ba168a345 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sat, 31 Jan 2026 23:45:09 -0500 Subject: [PATCH 19/39] Replace `fs`/`path` usage with Vite's `import.meta.glob` --- docs/src/lib/llms/utils.ts | 121 +++++++++++++----- .../[name]/[example]/llms.txt/+server.ts | 27 ++-- .../docs/getting-started/llms.txt/+server.ts | 4 +- .../docs/guides/[name]/llms.txt/+server.ts | 4 +- docs/src/routes/llms.txt/+server.ts | 52 +++----- 5 files changed, 124 insertions(+), 84 deletions(-) diff --git a/docs/src/lib/llms/utils.ts b/docs/src/lib/llms/utils.ts index 378d48fbd..27fa0c329 100644 --- a/docs/src/lib/llms/utils.ts +++ b/docs/src/lib/llms/utils.ts @@ -1,10 +1,42 @@ -import { readFileSync } from 'fs'; -import { join } from 'path'; import type { ComponentAPI } from '$lib/api-types.js'; import { allComponents, allUtils, allGuides } from 'content-collections'; import { sortCollection } from '$lib/collections.js'; import { processMarkdownContent } from '$lib/markdown/utils.js'; +// Use import.meta.glob to load files at build time (Cloudflare Workers compatible) +const exampleSources = import.meta.glob('/src/examples/**/*.svelte', { + eager: true, + query: '?raw', + import: 'default' +}); + +const apiFiles = import.meta.glob('/src/generated/api/*.json', { + eager: true, + query: '?raw', + import: 'default' +}); + +const guideSources = import.meta.glob('/src/content/guides/*.md', { + eager: true, + query: '?raw', + import: 'default' +}); + +const gettingStartedSource = import.meta.glob( + '/src/routes/docs/getting-started/+page.md', + { + eager: true, + query: '?raw', + import: 'default' + } +); + +const catalogFiles = import.meta.glob('/src/examples/catalog/*.json', { + eager: true, + query: '?raw', + import: 'default' +}); + export const BASE_URL = 'https://layerchart.com'; /** Generate URL for a docs page */ @@ -103,15 +135,10 @@ export function generateComponentMarkdown( // Load example let exampleSource = ''; if (component.usageExample) { - try { - const examplePath = join( - process.cwd(), - `src/examples/components/${component.slug}/${component.usageExample}.svelte` - ); - exampleSource = readFileSync(examplePath, 'utf-8'); - exampleSource = trimCode(exampleSource); - } catch (e) { - // Example file may not exist + const key = `/src/examples/components/${component.slug}/${component.usageExample}.svelte`; + const raw = exampleSources[key]; + if (raw) { + exampleSource = trimCode(raw); } } @@ -122,12 +149,10 @@ export function generateComponentMarkdown( // Load API let api: ComponentAPI | null = null; - try { - const apiPath = join(process.cwd(), `src/generated/api/${component.slug}.json`); - const apiContent = readFileSync(apiPath, 'utf-8'); + const apiKey = `/src/generated/api/${component.slug}.json`; + const apiContent = apiFiles[apiKey]; + if (apiContent) { api = JSON.parse(apiContent); - } catch (e) { - // API file may not exist } if (api) { @@ -179,15 +204,10 @@ export function generateUtilMarkdown( // Load example let exampleSource = ''; if (util.usageExample) { - try { - const examplePath = join( - process.cwd(), - `src/examples/utils/${util.slug}/${util.usageExample}.svelte` - ); - exampleSource = readFileSync(examplePath, 'utf-8'); - exampleSource = trimCode(exampleSource); - } catch (e) { - // Example file may not exist + const key = `/src/examples/utils/${util.slug}/${util.usageExample}.svelte`; + const raw = exampleSources[key]; + if (raw) { + exampleSource = trimCode(raw); } } @@ -235,21 +255,29 @@ export function generateGuidesSection(): string { export interface GenerateGuideMarkdownOptions { /** The name/slug of the guide (e.g., 'getting-started', 'styles') */ name: string; - /** The filesystem path to the markdown file */ - filePath: string; /** Optional explicit title. If not provided, title is extracted from frontmatter, * falling back to title-casing the name. */ title?: string; } /** - * Read a guide markdown file, extract its title, process the content, - * and return the final markdown string. + * Load and generate markdown for a guide. + * Uses import.meta.glob to read files (Cloudflare Workers compatible). */ export function generateGuideMarkdown(options: GenerateGuideMarkdownOptions): string { - const { name, filePath, title: explicitTitle } = options; + const { name, title: explicitTitle } = options; + + // Look up from the pre-loaded glob maps + let raw: string | undefined; + if (name === 'getting-started') { + raw = gettingStartedSource['/src/routes/docs/getting-started/+page.md']; + } else { + raw = guideSources[`/src/content/guides/${name}.md`]; + } - const raw = readFileSync(filePath, 'utf-8'); + if (!raw) { + throw new Error(`Guide "${name}" not found`); + } // Extract title from frontmatter if not explicitly provided let title = explicitTitle; @@ -273,6 +301,37 @@ export function generateGuideMarkdown(options: GenerateGuideMarkdownOptions): st return `# ${title}\n\n${content}`; } +/** + * Get the raw source for an example svelte file. + */ +export function getExampleSource( + type: 'components' | 'utils', + componentSlug: string, + exampleName: string +): string | undefined { + const key = `/src/examples/${type}/${componentSlug}/${exampleName}.svelte`; + return exampleSources[key]; +} + +/** + * Get the parsed catalog JSON for a component. + */ +export function getCatalog(componentSlug: string): Record | null { + const key = `/src/examples/catalog/${componentSlug}.json`; + const raw = catalogFiles[key]; + if (!raw) return null; + return JSON.parse(raw); +} + +/** + * Get all example file paths from the glob map (for listing). + */ +export function getAllExamplePaths(): string[] { + return Object.keys(exampleSources) + .filter((key) => key.startsWith('/src/examples/components/')) + .sort(); +} + export interface CollectionListOptions { title: string; items: Array<{ slug: string; name: string; description?: string }>; diff --git a/docs/src/routes/docs/components/[name]/[example]/llms.txt/+server.ts b/docs/src/routes/docs/components/[name]/[example]/llms.txt/+server.ts index 1efea694e..02b080a45 100644 --- a/docs/src/routes/docs/components/[name]/[example]/llms.txt/+server.ts +++ b/docs/src/routes/docs/components/[name]/[example]/llms.txt/+server.ts @@ -1,34 +1,29 @@ import { error } from '@sveltejs/kit'; -import { readFileSync } from 'fs'; -import { join } from 'path'; import type { RequestHandler } from './$types'; import { allComponents } from 'content-collections'; import type { ComponentCatalog } from '$examples/catalog/types.js'; import { processMarkdownContent } from '$lib/markdown/utils.js'; -import { llmsUrl, trimCode, markdownResponse } from '$lib/llms/utils.js'; +import { + llmsUrl, + trimCode, + markdownResponse, + getExampleSource, + getCatalog +} from '$lib/llms/utils.js'; export const GET: RequestHandler = async ({ params }) => { const { name, example } = params; const component = allComponents.find((c) => c.slug === name); - let exampleSource = ''; - try { - const examplePath = join(process.cwd(), `src/examples/components/${name}/${example}.svelte`); - exampleSource = readFileSync(examplePath, 'utf-8'); - exampleSource = trimCode(exampleSource); - } catch (e) { + const raw = getExampleSource('components', name, example); + if (!raw) { error(404, `Example "${example}" not found for component "${name}"`); } + const exampleSource = trimCode(raw); // Load catalog to find components used in this example - let catalog: ComponentCatalog | null = null; - try { - const catalogPath = join(process.cwd(), `src/examples/catalog/${name}.json`); - catalog = JSON.parse(readFileSync(catalogPath, 'utf-8')); - } catch (e) { - // Catalog may not exist - } + const catalog = getCatalog(name) as ComponentCatalog | null; const exampleInfo = catalog?.examples.find((e) => e.name === example); const usedComponentNames = [...new Set(exampleInfo?.components.map((c) => c.component) ?? [])]; diff --git a/docs/src/routes/docs/getting-started/llms.txt/+server.ts b/docs/src/routes/docs/getting-started/llms.txt/+server.ts index 30a4bb271..d30de3d26 100644 --- a/docs/src/routes/docs/getting-started/llms.txt/+server.ts +++ b/docs/src/routes/docs/getting-started/llms.txt/+server.ts @@ -1,10 +1,8 @@ -import { join } from 'path'; import type { RequestHandler } from './$types'; import { generateGuideMarkdown, markdownResponse } from '$lib/llms/utils.js'; export const GET: RequestHandler = async () => { - const filePath = join(process.cwd(), 'src/routes/docs/getting-started/+page.md'); - const markdown = generateGuideMarkdown({ name: 'getting-started', filePath, title: 'Getting Started' }); + const markdown = generateGuideMarkdown({ name: 'getting-started', title: 'Getting Started' }); return markdownResponse(markdown, 'getting-started.md'); }; diff --git a/docs/src/routes/docs/guides/[name]/llms.txt/+server.ts b/docs/src/routes/docs/guides/[name]/llms.txt/+server.ts index 7c220f269..9d3099d46 100644 --- a/docs/src/routes/docs/guides/[name]/llms.txt/+server.ts +++ b/docs/src/routes/docs/guides/[name]/llms.txt/+server.ts @@ -1,5 +1,4 @@ import { error } from '@sveltejs/kit'; -import { join } from 'path'; import type { RequestHandler } from './$types'; import { generateGuideMarkdown, markdownResponse } from '$lib/llms/utils.js'; @@ -8,8 +7,7 @@ export const GET: RequestHandler = async ({ params }) => { let markdown: string; try { - const filePath = join(process.cwd(), `src/content/guides/${name}.md`); - markdown = generateGuideMarkdown({ name, filePath }); + markdown = generateGuideMarkdown({ name }); } catch (e) { error(404, `Guide "${name}" not found`); } diff --git a/docs/src/routes/llms.txt/+server.ts b/docs/src/routes/llms.txt/+server.ts index 378bd280c..3e9130937 100644 --- a/docs/src/routes/llms.txt/+server.ts +++ b/docs/src/routes/llms.txt/+server.ts @@ -1,11 +1,10 @@ import { allComponents, allUtils } from 'content-collections'; -import { readdirSync } from 'fs'; -import { join } from 'path'; import type { RequestHandler } from './$types'; import { generateGuidesSection, generateCollectionListSection, + getAllExamplePaths, llmsUrl, textResponse } from '$lib/llms/utils.js'; @@ -26,8 +25,16 @@ function generateLlmsTxt(): string { This file contains links to LLM-optimized documentation in markdown format.`); sections.push(generateGuidesSection()); - sections.push(generateCollectionListSection({ title: 'Components', items: allComponents, type: 'components' })); - sections.push(generateCollectionListSection({ title: 'Utilities', items: allUtils, type: 'utils' })); + sections.push( + generateCollectionListSection({ + title: 'Components', + items: allComponents, + type: 'components' + }) + ); + sections.push( + generateCollectionListSection({ title: 'Utilities', items: allUtils, type: 'utils' }) + ); // Examples section const examplesList = getExamplesList(); @@ -37,38 +44,21 @@ This file contains links to LLM-optimized documentation in markdown format.`); } /** - * Get list of all examples from the filesystem + * Get list of all examples from import.meta.glob keys */ function getExamplesList(): string { - const examplesDir = join(process.cwd(), 'src/examples/components'); + const paths = getAllExamplePaths(); const examples: string[] = []; - try { - const componentDirs = readdirSync(examplesDir, { withFileTypes: true }) - .filter((dirent) => dirent.isDirectory()) - .map((dirent) => dirent.name) - .sort(); - - for (const componentName of componentDirs) { - const componentDir = join(examplesDir, componentName); - - try { - const exampleFiles = readdirSync(componentDir) - .filter((file) => file.endsWith('.svelte')) - .sort(); - - for (const exampleFile of exampleFiles) { - const exampleName = exampleFile.replace('.svelte', ''); - examples.push( - `- [${componentName}/${exampleName}](${llmsUrl('components', `${componentName}/${exampleName}`)}): Example code for ${componentName}` - ); - } - } catch (e) { - // Skip directories that can't be read - } + for (const path of paths) { + // path is like "/src/examples/components/Arc/Basic.svelte" + const match = path.match(/\/src\/examples\/components\/([^/]+)\/([^/]+)\.svelte$/); + if (match) { + const [, componentName, exampleName] = match; + examples.push( + `- [${componentName}/${exampleName}](${llmsUrl('components', `${componentName}/${exampleName}`)}): Example code for ${componentName}` + ); } - } catch (e) { - return 'Could not read examples directory.'; } return examples.join('\n'); From b812f53d6d22638159ab84f0aae312a0163a3b30 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sun, 1 Feb 2026 00:20:52 -0500 Subject: [PATCH 20/39] Move processMarkdownContent to llms/utils.ts --- docs/src/lib/llms/utils.ts | 103 +++++++++++++++- docs/src/lib/markdown/utils.ts | 113 ------------------ .../[name]/[example]/llms.txt/+server.ts | 2 +- .../components/[name]/llms.txt/+server.ts | 3 +- .../docs/utils/[name]/llms.txt/+server.ts | 3 +- 5 files changed, 104 insertions(+), 120 deletions(-) diff --git a/docs/src/lib/llms/utils.ts b/docs/src/lib/llms/utils.ts index 27fa0c329..e0ed845e6 100644 --- a/docs/src/lib/llms/utils.ts +++ b/docs/src/lib/llms/utils.ts @@ -1,8 +1,6 @@ import type { ComponentAPI } from '$lib/api-types.js'; import { allComponents, allUtils, allGuides } from 'content-collections'; import { sortCollection } from '$lib/collections.js'; -import { processMarkdownContent } from '$lib/markdown/utils.js'; - // Use import.meta.glob to load files at build time (Cloudflare Workers compatible) const exampleSources = import.meta.glob('/src/examples/**/*.svelte', { eager: true, @@ -63,6 +61,107 @@ export interface GuideEntry { description: string; } +/** + * Process markdown content for LLMs by removing custom syntax and converting to vanilla markdown + */ +export function processMarkdownContent(content: string): string { + // Remove frontmatter (YAML between --- markers at start of file) + content = content.replace(/^---\n[\s\S]*?\n---\n*/, ''); + + // Remove Svelte script blocks and components ONLY outside of code blocks + // Split by code blocks, process only non-code-block parts, then rejoin + content = content + .split(/(```[\s\S]*?```)/g) + .map((part, index) => { + // Odd indices are code blocks (matched by the capture group) + if (index % 2 === 1) return part; + // Remove Svelte script blocks + part = part.replace(/]*>[\s\S]*?<\/script>\n*/g, ''); + // Remove Svelte components (self-closing and with content) + part = part.replace(/<[A-Z][a-zA-Z]*[^>]*\/>\n*/g, ''); + part = part.replace(/<[A-Z][a-zA-Z]*[^>]*>[\s\S]*?<\/[A-Z][a-zA-Z]*>\n*/g, ''); + return part; + }) + .join(''); + + // Extract title from code blocks and add as "File:" line before (must run before tabs processing) + content = content.replace(/(```\w*)\s+([^\n]*title="[^"]+")[^\n]*$/gm, (_, lang, meta) => { + const titleMatch = meta.match(/title="([^"]+)"/); + if (titleMatch) { + return `File: ${titleMatch[1]} ${lang}`; + } + return lang; + }); + + // Process tabs - convert to table + content = content.replace( + /:::tabs\{key="([^"]+)"\}\s*([\s\S]*?)(?=\n:::(?:\s*$|\s*\n))\n:::/gm, + (_, key, tabsContent) => { + const tabs: { label: string; content: string }[] = []; + const tabRegex = + /::tab\{label="([^"]+)"[^}]*\}\s*([\s\S]*?)\s*(?=\n\s*::(?:\s*$|\s+))\n\s*::/gm; + let match; + while ((match = tabRegex.exec(tabsContent)) !== null) { + tabs.push({ label: match[1], content: match[2].trim() }); + } + + if (tabs.length === 0) return ''; + + // Build table with capitalized key as header + const header = key.charAt(0).toUpperCase() + key.slice(1); + let table = `| ${header} | Details |\n|-----------|---------|`; + for (const tab of tabs) { + // Clean up content: remove :button syntax, convert to links, unwrap code blocks + const cleanContent = tab.content + .replace(/:button\{label="([^"]+)"\s+href="([^"]+)"[^}]*\}/g, '[$1]($2)') + .replace(/```\w*\n([\s\S]*?)```/g, '$1') // Remove code block fences, keep content + .replace(/\n/g, ' ') + .trim(); + table += `\n| ${tab.label} | ${cleanContent} |`; + } + return table; + } + ); + + // Convert ::note/:::note, ::tip/:::tip, etc. to blockquote (2 or 3 colons) + content = content.replace( + /:{2,3}(note|tip|warning|caution)\s*([\s\S]*?)(?=\n:{2,3}(?:\s*$|\s*\n))\n:{2,3}/gm, + (_, variant, noteContent) => { + return `> ${variant}: ${noteContent.trim()}\n`; + } + ); + + // Convert ::steps to numbered list (convert ## headings to numbered items) + content = content.replace( + /::steps\s*([\s\S]*?)(?=\n::(?:\s*$|\s*\n))\n::/gm, + (_, stepsContent: string) => { + let stepNum = 0; + return stepsContent.replace(/^## (.+)$/gm, (_match: string, heading: string) => { + stepNum++; + return `**${stepNum}. ${heading}**`; + }); + } + ); + + // Remove any remaining standalone :: + content = content.replace(/^::\s*$/gm, ''); + + // Remove :icon syntax, keep text if in brackets, otherwise just remove icon + content = content.replace(/\[:icon\{[^}]+\}\s*([^\]]+)\]/g, '$1'); + content = content.replace(/:icon\{[^}]+\}\s*/g, ''); + + // Convert :example to reference link + content = content.replace( + /:example\{component="([^"]+)"\s+name="([^"]+)"[^}]*\}/g, + 'See example: [$1/$2](https://layerchart.com/docs/components/$1/$2)' + ); + + // Clean up multiple blank lines + content = content.replace(/\n{3,}/g, '\n\n'); + + return content.trim(); +} + /** * Trim code to remove module exports and data export statement */ diff --git a/docs/src/lib/markdown/utils.ts b/docs/src/lib/markdown/utils.ts index ef66bea4a..cc58aecfb 100644 --- a/docs/src/lib/markdown/utils.ts +++ b/docs/src/lib/markdown/utils.ts @@ -168,116 +168,3 @@ export async function loadExamplesFromMarkdown( return examples; } -/** - * Process content only outside of code blocks (``` ... ```) - * Preserves code block content unchanged while applying transformations elsewhere - */ -function processOutsideCodeBlocks(content: string, processor: (text: string) => string): string { - // Split by code blocks, preserving the delimiters - const parts = content.split(/(```[\s\S]*?```)/g); - - return parts - .map((part, index) => { - // Odd indices are code blocks (matched by the capture group) - if (index % 2 === 1) { - return part; // Keep code blocks unchanged - } - return processor(part); // Process non-code-block content - }) - .join(''); -} - -/** - * Process markdown content for LLMs by removing custom syntax and converting to vanilla markdown - */ -export function processMarkdownContent(content: string): string { - // Remove frontmatter (YAML between --- markers at start of file) - content = content.replace(/^---\n[\s\S]*?\n---\n*/, ''); - - // Remove Svelte script blocks and components ONLY outside of code blocks - content = processOutsideCodeBlocks(content, (text) => { - // Remove Svelte script blocks - text = text.replace(/]*>[\s\S]*?<\/script>\n*/g, ''); - // Remove Svelte components (self-closing and with content) - text = text.replace(/<[A-Z][a-zA-Z]*[^>]*\/>\n*/g, ''); - text = text.replace(/<[A-Z][a-zA-Z]*[^>]*>[\s\S]*?<\/[A-Z][a-zA-Z]*>\n*/g, ''); - return text; - }); - - // Extract title from code blocks and add as "File:" line before (must run before tabs processing) - content = content.replace(/(```\w*)\s+([^\n]*title="[^"]+")[^\n]*$/gm, (_, lang, meta) => { - const titleMatch = meta.match(/title="([^"]+)"/); - if (titleMatch) { - return `File: ${titleMatch[1]} ${lang}`; - } - return lang; - }); - - // Process tabs - convert to table - content = content.replace( - /:::tabs\{key="([^"]+)"\}\s*([\s\S]*?)(?=\n:::(?:\s*$|\s*\n))\n:::/gm, - (_, key, tabsContent) => { - const tabs: { label: string; content: string }[] = []; - const tabRegex = - /::tab\{label="([^"]+)"[^}]*\}\s*([\s\S]*?)\s*(?=\n\s*::(?:\s*$|\s+))\n\s*::/gm; - let match; - while ((match = tabRegex.exec(tabsContent)) !== null) { - tabs.push({ label: match[1], content: match[2].trim() }); - } - - if (tabs.length === 0) return ''; - - // Build table with capitalized key as header - const header = key.charAt(0).toUpperCase() + key.slice(1); - let table = `| ${header} | Details |\n|-----------|---------|`; - for (const tab of tabs) { - // Clean up content: remove :button syntax, convert to links, unwrap code blocks - const cleanContent = tab.content - .replace(/:button\{label="([^"]+)"\s+href="([^"]+)"[^}]*\}/g, '[$1]($2)') - .replace(/```\w*\n([\s\S]*?)```/g, '$1') // Remove code block fences, keep content - .replace(/\n/g, ' ') - .trim(); - table += `\n| ${tab.label} | ${cleanContent} |`; - } - return table; - } - ); - - // Convert ::note/:::note, ::tip/:::tip, etc. to blockquote (2 or 3 colons) - content = content.replace( - /:{2,3}(note|tip|warning|caution)\s*([\s\S]*?)(?=\n:{2,3}(?:\s*$|\s*\n))\n:{2,3}/gm, - (_, variant, noteContent) => { - return `> ${variant}: ${noteContent.trim()}\n`; - } - ); - - // Convert ::steps to numbered list (convert ## headings to numbered items) - content = content.replace( - /::steps\s*([\s\S]*?)(?=\n::(?:\s*$|\s*\n))\n::/gm, - (_, stepsContent: string) => { - let stepNum = 0; - return stepsContent.replace(/^## (.+)$/gm, (_match: string, heading: string) => { - stepNum++; - return `**${stepNum}. ${heading}**`; - }); - } - ); - - // Remove any remaining standalone :: - content = content.replace(/^::\s*$/gm, ''); - - // Remove :icon syntax, keep text if in brackets, otherwise just remove icon - content = content.replace(/\[:icon\{[^}]+\}\s*([^\]]+)\]/g, '$1'); - content = content.replace(/:icon\{[^}]+\}\s*/g, ''); - - // Convert :example to reference link - content = content.replace( - /:example\{component="([^"]+)"\s+name="([^"]+)"[^}]*\}/g, - 'See example: [$1/$2](https://layerchart.com/docs/components/$1/$2)' - ); - - // Clean up multiple blank lines - content = content.replace(/\n{3,}/g, '\n\n'); - - return content.trim(); -} diff --git a/docs/src/routes/docs/components/[name]/[example]/llms.txt/+server.ts b/docs/src/routes/docs/components/[name]/[example]/llms.txt/+server.ts index 02b080a45..9fbd050eb 100644 --- a/docs/src/routes/docs/components/[name]/[example]/llms.txt/+server.ts +++ b/docs/src/routes/docs/components/[name]/[example]/llms.txt/+server.ts @@ -2,8 +2,8 @@ import { error } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; import { allComponents } from 'content-collections'; import type { ComponentCatalog } from '$examples/catalog/types.js'; -import { processMarkdownContent } from '$lib/markdown/utils.js'; import { + processMarkdownContent, llmsUrl, trimCode, markdownResponse, diff --git a/docs/src/routes/docs/components/[name]/llms.txt/+server.ts b/docs/src/routes/docs/components/[name]/llms.txt/+server.ts index c4b5aaa39..dafadd41d 100644 --- a/docs/src/routes/docs/components/[name]/llms.txt/+server.ts +++ b/docs/src/routes/docs/components/[name]/llms.txt/+server.ts @@ -1,8 +1,7 @@ import { error } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; import { allComponents } from 'content-collections'; -import { processMarkdownContent } from '$lib/markdown/utils.js'; -import { generateComponentMarkdown, markdownResponse } from '$lib/llms/utils.js'; +import { processMarkdownContent, generateComponentMarkdown, markdownResponse } from '$lib/llms/utils.js'; export const GET: RequestHandler = async ({ params }) => { const { name } = params; diff --git a/docs/src/routes/docs/utils/[name]/llms.txt/+server.ts b/docs/src/routes/docs/utils/[name]/llms.txt/+server.ts index def2373a1..e4ce19646 100644 --- a/docs/src/routes/docs/utils/[name]/llms.txt/+server.ts +++ b/docs/src/routes/docs/utils/[name]/llms.txt/+server.ts @@ -1,8 +1,7 @@ import { error } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; import { allUtils } from 'content-collections'; -import { processMarkdownContent } from '$lib/markdown/utils.js'; -import { generateUtilMarkdown, markdownResponse } from '$lib/llms/utils.js'; +import { processMarkdownContent, generateUtilMarkdown, markdownResponse } from '$lib/llms/utils.js'; export const GET: RequestHandler = async ({ params }) => { const { name } = params; From 7a9411713172543156c44965429384dca3db3ad5 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sun, 1 Feb 2026 00:28:54 -0500 Subject: [PATCH 21/39] processMarkdownContent within generateComponentMarkdown / generateUtilMarkdown --- docs/src/lib/llms/utils.ts | 33 +++++++++++-------- .../[name]/[example]/llms.txt/+server.ts | 4 +-- .../components/[name]/llms.txt/+server.ts | 5 ++- .../docs/utils/[name]/llms.txt/+server.ts | 5 ++- 4 files changed, 24 insertions(+), 23 deletions(-) diff --git a/docs/src/lib/llms/utils.ts b/docs/src/lib/llms/utils.ts index e0ed845e6..27ccdfd4c 100644 --- a/docs/src/lib/llms/utils.ts +++ b/docs/src/lib/llms/utils.ts @@ -20,14 +20,11 @@ const guideSources = import.meta.glob('/src/content/guides/*.md', { import: 'default' }); -const gettingStartedSource = import.meta.glob( - '/src/routes/docs/getting-started/+page.md', - { - eager: true, - query: '?raw', - import: 'default' - } -); +const gettingStartedSource = import.meta.glob('/src/routes/docs/getting-started/+page.md', { + eager: true, + query: '?raw', + import: 'default' +}); const catalogFiles = import.meta.glob('/src/examples/catalog/*.json', { eager: true, @@ -226,10 +223,13 @@ export function generateComponentMarkdown( sections.push(`**Supported Layers:** ${component.layers.join(', ')}`); } - // Documentation link - sections.push( - `**Full Documentation:** [${component.name}](${docsUrl('components', component.slug)})` - ); + // Documentation content from markdown + if (component.content) { + const processed = processMarkdownContent(component.content); + if (processed) { + sections.push(processed); + } + } // Load example let exampleSource = ''; @@ -297,8 +297,13 @@ export function generateUtilMarkdown( sections.push(util.description); } - // Documentation link - sections.push(`**Full Documentation:** [${util.name}](${docsUrl('utils', util.slug)})`); + // Documentation content from markdown + if (util.content) { + const processed = processMarkdownContent(util.content); + if (processed) { + sections.push(processed); + } + } // Load example let exampleSource = ''; diff --git a/docs/src/routes/docs/components/[name]/[example]/llms.txt/+server.ts b/docs/src/routes/docs/components/[name]/[example]/llms.txt/+server.ts index 9fbd050eb..b2f93fc19 100644 --- a/docs/src/routes/docs/components/[name]/[example]/llms.txt/+server.ts +++ b/docs/src/routes/docs/components/[name]/[example]/llms.txt/+server.ts @@ -3,7 +3,6 @@ import type { RequestHandler } from './$types'; import { allComponents } from 'content-collections'; import type { ComponentCatalog } from '$examples/catalog/types.js'; import { - processMarkdownContent, llmsUrl, trimCode, markdownResponse, @@ -31,8 +30,7 @@ export const GET: RequestHandler = async ({ params }) => { .map((c) => allComponents.find((ac) => ac.name === c)) .filter((c) => c != null); - let markdown = generateMarkdown(name, example, component, exampleSource, usedComponents); - markdown = processMarkdownContent(markdown); + const markdown = generateMarkdown(name, example, component, exampleSource, usedComponents); return markdownResponse(markdown, `${name}-${example}.md`); }; diff --git a/docs/src/routes/docs/components/[name]/llms.txt/+server.ts b/docs/src/routes/docs/components/[name]/llms.txt/+server.ts index dafadd41d..7f1733de5 100644 --- a/docs/src/routes/docs/components/[name]/llms.txt/+server.ts +++ b/docs/src/routes/docs/components/[name]/llms.txt/+server.ts @@ -1,7 +1,7 @@ import { error } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; import { allComponents } from 'content-collections'; -import { processMarkdownContent, generateComponentMarkdown, markdownResponse } from '$lib/llms/utils.js'; +import { generateComponentMarkdown, markdownResponse } from '$lib/llms/utils.js'; export const GET: RequestHandler = async ({ params }) => { const { name } = params; @@ -11,8 +11,7 @@ export const GET: RequestHandler = async ({ params }) => { error(404, `Component "${name}" not found`); } - let markdown = generateComponentMarkdown(component); - markdown = processMarkdownContent(markdown); + const markdown = generateComponentMarkdown(component); return markdownResponse(markdown, `${name}.md`); }; diff --git a/docs/src/routes/docs/utils/[name]/llms.txt/+server.ts b/docs/src/routes/docs/utils/[name]/llms.txt/+server.ts index e4ce19646..dc4c2c30f 100644 --- a/docs/src/routes/docs/utils/[name]/llms.txt/+server.ts +++ b/docs/src/routes/docs/utils/[name]/llms.txt/+server.ts @@ -1,7 +1,7 @@ import { error } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; import { allUtils } from 'content-collections'; -import { processMarkdownContent, generateUtilMarkdown, markdownResponse } from '$lib/llms/utils.js'; +import { generateUtilMarkdown, markdownResponse } from '$lib/llms/utils.js'; export const GET: RequestHandler = async ({ params }) => { const { name } = params; @@ -11,8 +11,7 @@ export const GET: RequestHandler = async ({ params }) => { error(404, `Utility "${name}" not found`); } - let markdown = generateUtilMarkdown(util); - markdown = processMarkdownContent(markdown); + const markdown = generateUtilMarkdown(util); return markdownResponse(markdown, `${name}.md`); }; From 163652811e48982c17f6432ac8b40f324ab580ea Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sun, 1 Feb 2026 00:44:21 -0500 Subject: [PATCH 22/39] include `:example { }`directives --- docs/src/lib/llms/utils.ts | 81 ++++++++++++++----- .../components/[name]/llms.txt/+server.ts | 2 +- .../docs/utils/[name]/llms.txt/+server.ts | 2 +- 3 files changed, 63 insertions(+), 22 deletions(-) diff --git a/docs/src/lib/llms/utils.ts b/docs/src/lib/llms/utils.ts index 27ccdfd4c..2a42d37ee 100644 --- a/docs/src/lib/llms/utils.ts +++ b/docs/src/lib/llms/utils.ts @@ -50,6 +50,8 @@ export function llmsUrl(type: 'components' | 'utils' | 'guides', slug: string): export interface GenerateMarkdownOptions { /** Heading level for the title. 1 = "#", 2 = "##", etc. Default: 1 */ headingLevel?: number; + /** Whether to inline all example code blocks. Default: false */ + inlineExamples?: boolean; } export interface GuideEntry { @@ -58,6 +60,46 @@ export interface GuideEntry { description: string; } +/** + * Replace :example{...} directives with inlined code blocks. + * Must be called BEFORE processMarkdownContent. + */ +function inlineExampleDirectives( + content: string, + slug: string, + type: 'components' | 'utils' +): string { + // Remove HTML comments first to avoid inlining commented-out examples + content = content.replace(//g, ''); + + // Cross-component examples: :example{ component="X" name="Y" ... } + // Must run before same-component regex to avoid greedy matching + content = content.replace( + /:example\{\s*component="([^"]+)"\s+name="([^"]+)"[^}]*\}/g, + (_match, component: string, name: string) => { + const raw = getExampleSource('components', component, name); + if (raw) { + return '```svelte\n' + trimCode(raw) + '\n```'; + } + return `See example: [${component}/${name}](${BASE_URL}/docs/components/${component}/${name})`; + } + ); + + // Same-component examples: :example{ name="Y" ... } + content = content.replace( + /:example\{\s*name="([^"]+)"[^}]*\}/g, + (_match, name: string) => { + const raw = getExampleSource(type, slug, name); + if (raw) { + return '```svelte\n' + trimCode(raw) + '\n```'; + } + return `See example: ${name}`; + } + ); + + return content; +} + /** * Process markdown content for LLMs by removing custom syntax and converting to vanilla markdown */ @@ -65,6 +107,9 @@ export function processMarkdownContent(content: string): string { // Remove frontmatter (YAML between --- markers at start of file) content = content.replace(/^---\n[\s\S]*?\n---\n*/, ''); + // Remove HTML comments + content = content.replace(//g, ''); + // Remove Svelte script blocks and components ONLY outside of code blocks // Split by code blocks, process only non-code-block parts, then rejoin content = content @@ -147,12 +192,15 @@ export function processMarkdownContent(content: string): string { content = content.replace(/\[:icon\{[^}]+\}\s*([^\]]+)\]/g, '$1'); content = content.replace(/:icon\{[^}]+\}\s*/g, ''); - // Convert :example to reference link + // Convert :example with component to reference link content = content.replace( /:example\{component="([^"]+)"\s+name="([^"]+)"[^}]*\}/g, 'See example: [$1/$2](https://layerchart.com/docs/components/$1/$2)' ); + // Convert remaining :example directives (same-component, not inlined) to plain text + content = content.replace(/:example\{\s*name="([^"]+)"[^}]*\}/g, 'See example: $1'); + // Clean up multiple blank lines content = content.replace(/\n{3,}/g, '\n\n'); @@ -204,7 +252,7 @@ export function generateComponentMarkdown( component: (typeof allComponents)[number], options: GenerateMarkdownOptions = {} ): string { - const { headingLevel = 1 } = options; + const { headingLevel = 1, inlineExamples: shouldInlineExamples = false } = options; const h = (level: number) => '#'.repeat(level); const sections: string[] = []; @@ -225,27 +273,16 @@ export function generateComponentMarkdown( // Documentation content from markdown if (component.content) { - const processed = processMarkdownContent(component.content); + let rawContent = component.content; + if (shouldInlineExamples) { + rawContent = inlineExampleDirectives(rawContent, component.slug, 'components'); + } + const processed = processMarkdownContent(rawContent); if (processed) { sections.push(processed); } } - // Load example - let exampleSource = ''; - if (component.usageExample) { - const key = `/src/examples/components/${component.slug}/${component.usageExample}.svelte`; - const raw = exampleSources[key]; - if (raw) { - exampleSource = trimCode(raw); - } - } - - if (exampleSource) { - sections.push(`${h(headingLevel + 1)} Example`); - sections.push('```svelte\n' + exampleSource + '\n```'); - } - // Load API let api: ComponentAPI | null = null; const apiKey = `/src/generated/api/${component.slug}.json`; @@ -286,7 +323,7 @@ export function generateUtilMarkdown( util: (typeof allUtils)[number], options: GenerateMarkdownOptions = {} ): string { - const { headingLevel = 1 } = options; + const { headingLevel = 1, inlineExamples: shouldInlineExamples = false } = options; const h = (level: number) => '#'.repeat(level); const sections: string[] = []; @@ -299,7 +336,11 @@ export function generateUtilMarkdown( // Documentation content from markdown if (util.content) { - const processed = processMarkdownContent(util.content); + let rawContent = util.content; + if (shouldInlineExamples) { + rawContent = inlineExampleDirectives(rawContent, util.slug, 'utils'); + } + const processed = processMarkdownContent(rawContent); if (processed) { sections.push(processed); } diff --git a/docs/src/routes/docs/components/[name]/llms.txt/+server.ts b/docs/src/routes/docs/components/[name]/llms.txt/+server.ts index 7f1733de5..2828f614c 100644 --- a/docs/src/routes/docs/components/[name]/llms.txt/+server.ts +++ b/docs/src/routes/docs/components/[name]/llms.txt/+server.ts @@ -11,7 +11,7 @@ export const GET: RequestHandler = async ({ params }) => { error(404, `Component "${name}" not found`); } - const markdown = generateComponentMarkdown(component); + const markdown = generateComponentMarkdown(component, { inlineExamples: true }); return markdownResponse(markdown, `${name}.md`); }; diff --git a/docs/src/routes/docs/utils/[name]/llms.txt/+server.ts b/docs/src/routes/docs/utils/[name]/llms.txt/+server.ts index dc4c2c30f..40af7be0d 100644 --- a/docs/src/routes/docs/utils/[name]/llms.txt/+server.ts +++ b/docs/src/routes/docs/utils/[name]/llms.txt/+server.ts @@ -11,7 +11,7 @@ export const GET: RequestHandler = async ({ params }) => { error(404, `Utility "${name}" not found`); } - const markdown = generateUtilMarkdown(util); + const markdown = generateUtilMarkdown(util, { inlineExamples: true }); return markdownResponse(markdown, `${name}.md`); }; From 90b0c174eb5a30760f9ac65507de3166e2720d0f Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sun, 1 Feb 2026 00:51:35 -0500 Subject: [PATCH 23/39] cleanup --- docs/src/routes/docs/getting-started/llms.txt/+server.ts | 1 - docs/src/routes/docs/utils/[name]/llms.txt/+server.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/docs/src/routes/docs/getting-started/llms.txt/+server.ts b/docs/src/routes/docs/getting-started/llms.txt/+server.ts index d30de3d26..a0cd86c0c 100644 --- a/docs/src/routes/docs/getting-started/llms.txt/+server.ts +++ b/docs/src/routes/docs/getting-started/llms.txt/+server.ts @@ -3,6 +3,5 @@ import { generateGuideMarkdown, markdownResponse } from '$lib/llms/utils.js'; export const GET: RequestHandler = async () => { const markdown = generateGuideMarkdown({ name: 'getting-started', title: 'Getting Started' }); - return markdownResponse(markdown, 'getting-started.md'); }; diff --git a/docs/src/routes/docs/utils/[name]/llms.txt/+server.ts b/docs/src/routes/docs/utils/[name]/llms.txt/+server.ts index 40af7be0d..e74d05837 100644 --- a/docs/src/routes/docs/utils/[name]/llms.txt/+server.ts +++ b/docs/src/routes/docs/utils/[name]/llms.txt/+server.ts @@ -12,6 +12,5 @@ export const GET: RequestHandler = async ({ params }) => { } const markdown = generateUtilMarkdown(util, { inlineExamples: true }); - return markdownResponse(markdown, `${name}.md`); }; From 70a518d2d4689cb079d2192a17777cd26580b58e Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sun, 1 Feb 2026 00:55:42 -0500 Subject: [PATCH 24/39] move related section to bottom of component docs (also matches llms.txt) --- .../routes/docs/components/[name]/+page.svelte | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/src/routes/docs/components/[name]/+page.svelte b/docs/src/routes/docs/components/[name]/+page.svelte index 40bbe0e0d..1db160e16 100644 --- a/docs/src/routes/docs/components/[name]/+page.svelte +++ b/docs/src/routes/docs/components/[name]/+page.svelte @@ -150,15 +150,6 @@ {/if} {/if} -{#if metadata.related.length} -

Related

-
- {#each metadata.related as related} - - {/each} -
-{/if} - {#if api?.properties.length}

API Reference

@@ -231,3 +222,12 @@
{/if} {/if} + +{#if metadata.related.length} +

Related

+
+ {#each metadata.related as related} + + {/each} +
+{/if} From e613d0ab8e9e1fdea525956a6f2ec22e2d678113 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sun, 1 Feb 2026 01:05:18 -0500 Subject: [PATCH 25/39] Add examples to component llms.txt --- docs/src/lib/llms/utils.ts | 57 ++++++++++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 9 deletions(-) diff --git a/docs/src/lib/llms/utils.ts b/docs/src/lib/llms/utils.ts index 2a42d37ee..1b1cfa584 100644 --- a/docs/src/lib/llms/utils.ts +++ b/docs/src/lib/llms/utils.ts @@ -86,16 +86,13 @@ function inlineExampleDirectives( ); // Same-component examples: :example{ name="Y" ... } - content = content.replace( - /:example\{\s*name="([^"]+)"[^}]*\}/g, - (_match, name: string) => { - const raw = getExampleSource(type, slug, name); - if (raw) { - return '```svelte\n' + trimCode(raw) + '\n```'; - } - return `See example: ${name}`; + content = content.replace(/:example\{\s*name="([^"]+)"[^}]*\}/g, (_match, name: string) => { + const raw = getExampleSource(type, slug, name); + if (raw) { + return '```svelte\n' + trimCode(raw) + '\n```'; } - ); + return `See example: ${name}`; + }); return content; } @@ -304,6 +301,48 @@ export function generateComponentMarkdown( } } + // Examples from catalog + const catalog = getCatalog(component.slug); + if (catalog) { + const examples = catalog.examples as Array<{ name: string; path: string }>; + if (examples && examples.length > 0) { + sections.push(`${h(headingLevel + 1)} Examples`); + const exampleLinks = examples + .map((ex) => `- [${ex.name}](${BASE_URL}/docs/components/${component.slug}/${ex.name})`) + .join('\n'); + sections.push(exampleLinks); + } + + // TODO: should we include usage examples? + // Additional usage in other component examples (deduplicated) + // const usage = catalog.usage as Array<{ example: string; component: string; path: string }>; + // if (usage && usage.length > 0) { + // const exampleNames = new Set(examples?.map((ex) => ex.name) ?? []); + // const seen = new Set(); + // const uniqueUsage = usage.filter((item) => { + // // Exclude if already shown in main examples + // if (item.component === catalog.component && exampleNames.has(item.example)) { + // return false; + // } + // const key = `${item.component}::${item.example}`; + // if (seen.has(key)) return false; + // seen.add(key); + // return true; + // }); + + // if (uniqueUsage.length > 0) { + // sections.push(`${h(headingLevel + 1)} Additional Usage`); + // const usageLinks = uniqueUsage + // .map( + // (u) => + // `- [${u.component}/${u.example}](${BASE_URL}/docs/components/${u.component}/${u.example})` + // ) + // .join('\n'); + // sections.push(usageLinks); + // } + // } + } + // Related if (component.related && component.related.length > 0) { sections.push(`${h(headingLevel + 1)} Related`); From af80b709cd4f80bc31d088afeb8742139aaa0d31 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sun, 1 Feb 2026 01:09:36 -0500 Subject: [PATCH 26/39] Rename `Component docs` to just `Components` when viewing an example --- docs/src/routes/docs/components/[name]/[example]/+page.svelte | 2 +- .../docs/components/[name]/[example]/llms.txt/+server.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/src/routes/docs/components/[name]/[example]/+page.svelte b/docs/src/routes/docs/components/[name]/[example]/+page.svelte index 8a7b0a37f..7e73f8dcd 100644 --- a/docs/src/routes/docs/components/[name]/[example]/+page.svelte +++ b/docs/src/routes/docs/components/[name]/[example]/+page.svelte @@ -23,7 +23,7 @@ -

Component Docs

+

Components

{#each exampleInfo?.components as componentUsage}
From c3da9f1e1999130d2d476106727d3deae2f2d1fc Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sun, 1 Feb 2026 10:25:27 -0500 Subject: [PATCH 38/39] Update LLMs docs --- docs/src/content/guides/LLMs.md | 24 +++++++++++++++++------- docs/src/routes/+page.svelte | 2 +- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/docs/src/content/guides/LLMs.md b/docs/src/content/guides/LLMs.md index e1c6f811f..a6ad23d23 100644 --- a/docs/src/content/guides/LLMs.md +++ b/docs/src/content/guides/LLMs.md @@ -7,13 +7,13 @@ order: 8 import OpenWithButton from "$lib/components/OpenWithButton.svelte"; -The Layerchart documentation pages are designed to be accessible for humans developers using LLMs as well as large language models (LLMs) ingesting training data. +The LayerChart documentation pages are designed to be accessible for humans developers using LLMs as well as large language models (LLMs) ingesting training data. ## :icon{name="lucide:user" class="relative -top-1"} For the Humans -At the top of each documentation page, and demonstrated above, you'll find a button which copies the content of the page's documentation in Markdown to the clipboard. The convenient dropdown gives you additional helpful options. +At the top of each documentation page, and demonstrated above, you will find a button which copies the content of the page's documentation in Markdown to the clipboard. The convenient dropdown also gives you additional helpful options, such as viewing the component source or opening in chat. ::note The option for `View Component Source` is only shown for component pages. @@ -24,19 +24,29 @@ The option for `View Component Source` is only shown for component pages. LayerChart adopts the [llms.txt](https://llmstxt.org/) proposal standard, which provides a structured, machine-readable format optimized for LLMs. This enables developers, researchers, and AI systems to efficiently parse and utilize our documentation. ::note -Not all pages may support the `/llms.txt` suffix (ie those deemed irrelevant to LLMs). +Most but not all pages support the `/llms.txt` suffix (i.e. those deemed irrelevant to LLMs). :: ## LLM-friendly Documentation 3 Ways -1. To access the LLM-friendly version of supported Layerchart documentation pages, simply append `/llms.txt` to the end of the page's URL. This will return the content in a plain-text, LLM-optimized format. This is the same text which is copied to the clipboard when you click the `Copy Page` button. +::steps + +## Per page / component + +To access the LLM-friendly version of supported documentation pages, simply append `/llms.txt` to the end of the page's URL. This will return the content in a plain-text, LLM-optimized format. This is the same text which is copied to the clipboard when you click the `Copy Page` button. :::tip **Standard Page**: The LineChart component documentation is available at [/docs/components/LineChart](/docs/components/LineChart) -**LLM-friendly Version**: is available at [/docs/components/Linechart/llms.txt](/docs/components/LineChart/llms.txt) +**LLM-friendly Version**: is available at [/docs/components/LineChart/llms.txt](/docs/components/LineChart/llms.txt) ::: -1. To explore all supported pages in LLM-friendly format, visit the root index at [layerchart.com/llms.txt](/llms.txt). This page provides a comprehensive list of available documentation endpoints compatible with the `llms.txt` standard. +## Root Index + +To explore all supported pages in LLM-friendly format, visit the root index at [llms.txt](/llms.txt). This page provides a comprehensive list of available documentation endpoints compatible with the `llms.txt` standard. -1. For a complete, consolidated view of the all the Layerchart documentation in an LLM-friendly format, navigate to [layerchart.com/docs/llms.txt](/docs/llms.txt). This single endpoint aggregates all documentation content into a machine-readable structure, ideal for bulk processing or ingestion into AI systems. +## Complete Documentation + +For a complete, consolidated view of the all the documentation in an LLM-friendly format, navigate to [/docs/llms.txt](/docs/llms.txt). This single endpoint aggregates all documentation content into a machine-readable structure, ideal for bulk processing or ingestion into AI systems. + +:: diff --git a/docs/src/routes/+page.svelte b/docs/src/routes/+page.svelte index b566bef07..4f0378754 100644 --- a/docs/src/routes/+page.svelte +++ b/docs/src/routes/+page.svelte @@ -271,7 +271,7 @@
From 275aff224fc9020dafbe55e7c6fa39f21d66f5f3 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sun, 1 Feb 2026 10:49:49 -0500 Subject: [PATCH 39/39] Disable docs tests until contentCollections() can be enabled without triggering `__dirname is not defined` --- .github/workflows/ci.yml | 58 ++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 32e77c136..15fbfc395 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,36 +55,36 @@ jobs: env: CI: true - test-docs-server-ssr: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4.0.0 - - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - cache: pnpm - - run: pnpm install --frozen-lockfile - - name: Run docs unit tests (server + ssr) - run: pnpm --filter docs test:unit --project server --project ssr - env: - CI: true + # test-docs-server-ssr: + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v4 + # - uses: pnpm/action-setup@v4.0.0 + # - uses: actions/setup-node@v4 + # with: + # node-version: ${{ env.NODE_VERSION }} + # cache: pnpm + # - run: pnpm install --frozen-lockfile + # - name: Run docs unit tests (server + ssr) + # run: pnpm --filter docs test:unit --project server --project ssr + # env: + # CI: true - test-docs-browser: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4.0.0 - - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - cache: pnpm - - run: pnpm install --frozen-lockfile - - run: pnpm --filter docs exec playwright install chromium - - name: Run docs browser tests (client) - run: pnpm --filter docs test:unit --project client - env: - CI: true + # test-docs-browser: + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v4 + # - uses: pnpm/action-setup@v4.0.0 + # - uses: actions/setup-node@v4 + # with: + # node-version: ${{ env.NODE_VERSION }} + # cache: pnpm + # - run: pnpm install --frozen-lockfile + # - run: pnpm --filter docs exec playwright install chromium + # - name: Run docs browser tests (client) + # run: pnpm --filter docs test:unit --project client + # env: + # CI: true build: runs-on: ubuntu-latest