diff --git a/cspell.json b/cspell.json
index 6c381b4df..f97829712 100644
--- a/cspell.json
+++ b/cspell.json
@@ -114,6 +114,9 @@
"listbox",
"listitem",
"litelement",
+ "llm",
+ "llms",
+ "llmstxt",
"longform",
"lowercasing",
"Lucide",
diff --git a/package-lock.json b/package-lock.json
index 61dcacd43..c4485d1e6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14040,6 +14040,7 @@
"@wc-toolkit/jsx-types": "^1.3.0",
"eleventy-plugin-git-commit-date": "^0.1.3",
"esbuild": "^0.25.11",
+ "gray-matter": "^4.0.3",
"npm-check-updates": "^19.1.2"
},
"engines": {
diff --git a/packages/webawesome/custom-elements-manifest.js b/packages/webawesome/custom-elements-manifest.js
index c58530abd..53b9f596d 100644
--- a/packages/webawesome/custom-elements-manifest.js
+++ b/packages/webawesome/custom-elements-manifest.js
@@ -7,6 +7,7 @@ import fs from 'fs';
import * as path from 'node:path';
import { pascalCase } from 'pascal-case';
import * as url from 'url';
+import { llmsTxtPlugin } from './scripts/llms.js';
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
const packageData = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
@@ -188,6 +189,13 @@ export default {
},
}),
+ // Generate llms.txt
+ llmsTxtPlugin({
+ outdir,
+ docsDir: path.join(__dirname, 'docs'),
+ baseUrl: 'https://webawesome.com',
+ }),
+
//
// TODO - figure out why this broke when events were updated
//
diff --git a/packages/webawesome/docs/docs/resources/changelog.md b/packages/webawesome/docs/docs/resources/changelog.md
index 012763d8d..cae3d72ff 100644
--- a/packages/webawesome/docs/docs/resources/changelog.md
+++ b/packages/webawesome/docs/docs/resources/changelog.md
@@ -13,6 +13,7 @@ Components with the Experimental badge sh
## Next
+- Added llms.txt to assist AI agents with using Web Awesome [discuss:1100]
- Fixed a bug in `` that prevented the listbox from opening when options were preselected [issue:1883]
## 3.1.0
diff --git a/packages/webawesome/package.json b/packages/webawesome/package.json
index 36f9bf483..9e2e12ed1 100644
--- a/packages/webawesome/package.json
+++ b/packages/webawesome/package.json
@@ -92,6 +92,7 @@
"@wc-toolkit/jsx-types": "^1.3.0",
"eleventy-plugin-git-commit-date": "^0.1.3",
"esbuild": "^0.25.11",
+ "gray-matter": "^4.0.3",
"npm-check-updates": "^19.1.2"
}
}
diff --git a/packages/webawesome/scripts/llms.js b/packages/webawesome/scripts/llms.js
new file mode 100644
index 000000000..65aa06d2a
--- /dev/null
+++ b/packages/webawesome/scripts/llms.js
@@ -0,0 +1,257 @@
+import fs from 'fs';
+import matter from 'gray-matter';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import { getAllComponents } from './shared.js';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+/** Removes newlines from text to keep llms.txt formatting clean. */
+function removeNewlines(str) {
+ return str ? str.replace(/\n/g, ' ').trim() : '';
+}
+
+/** Loads front-matter from all component markdown files. */
+function loadAllFrontMatter(components, docsDir) {
+ const cache = new Map();
+
+ for (const component of components) {
+ const componentName = component.tagName.replace(/^wa-/, '');
+ const mdPath = path.join(docsDir, 'docs/components', `${componentName}.md`);
+
+ if (fs.existsSync(mdPath)) {
+ try {
+ const content = fs.readFileSync(mdPath, 'utf-8');
+ const { data } = matter(content);
+ cache.set(component.tagName, data);
+ } catch {
+ // Skip if parsing fails
+ }
+ }
+ }
+
+ return cache;
+}
+
+/** Generates the API reference section for a single component. */
+function generateComponentApiSection(component, frontMatterCache, baseUrl) {
+ const lines = [];
+ const frontMatter = frontMatterCache.get(component.tagName);
+ const componentSlug = component.tagName.replace(/^wa-/, '');
+ const description = removeNewlines(frontMatter?.description || component.summary || '');
+
+ lines.push(`#### \`<${component.tagName}>\``);
+ lines.push('');
+ lines.push(`**Description:** ${description || 'No description available.'}`);
+ lines.push('');
+ lines.push(`**Documentation:** ${baseUrl}/docs/components/${componentSlug}`);
+ lines.push('');
+
+ // Slots
+ if (component.slots?.length > 0) {
+ lines.push('**Slots:**');
+ lines.push('');
+ for (const slot of component.slots) {
+ const slotName = slot.name || '(default)';
+ lines.push(`- \`${slotName}\`: ${removeNewlines(slot.description) || 'No description.'}`);
+ }
+ lines.push('');
+ }
+
+ // Properties
+ const properties =
+ component.members?.filter(m => m.kind === 'field' && m.privacy !== 'private' && m.description) || [];
+
+ if (properties.length > 0) {
+ lines.push('**Properties:**');
+ lines.push('');
+ for (const prop of properties) {
+ // Find corresponding attribute if any
+ const attr = component.attributes?.find(a => a.fieldName === prop.name);
+ const attrNote = attr && attr.name !== prop.name ? ` (attribute: \`${attr.name}\`)` : '';
+ const typeStr = prop.type?.text ? `Type: \`${removeNewlines(prop.type.text)}\`` : '';
+ const defaultStr = prop.default ? `Default: \`${prop.default}\`` : '';
+ const meta = [typeStr, defaultStr].filter(Boolean).join(', ');
+
+ lines.push(
+ `- \`${prop.name}\`${attrNote}: ${removeNewlines(prop.description) || 'No description.'}${meta ? ` (${meta})` : ''}`,
+ );
+ }
+ lines.push('');
+ }
+
+ // Methods
+ const methods = component.members?.filter(m => m.kind === 'method' && m.privacy !== 'private' && m.description) || [];
+
+ if (methods.length > 0) {
+ lines.push('**Methods:**');
+ lines.push('');
+ for (const method of methods) {
+ const params = method.parameters?.length
+ ? `(${method.parameters.map(p => `${p.name}: ${removeNewlines(p.type?.text) || 'unknown'}`).join(', ')})`
+ : '()';
+ lines.push(`- \`${method.name}${params}\`: ${removeNewlines(method.description) || 'No description.'}`);
+ }
+ lines.push('');
+ }
+
+ // Events
+ const events = component.events?.filter(e => e.name) || [];
+ if (events.length > 0) {
+ lines.push('**Events:**');
+ lines.push('');
+ for (const event of events) {
+ lines.push(`- \`${event.name}\`: ${removeNewlines(event.description) || 'No description.'}`);
+ }
+ lines.push('');
+ }
+
+ // CSS Custom Properties
+ if (component.cssProperties?.length > 0) {
+ lines.push('**CSS Custom Properties:**');
+ lines.push('');
+ for (const prop of component.cssProperties) {
+ const defaultStr = prop.default ? ` (Default: \`${prop.default}\`)` : '';
+ lines.push(`- \`${prop.name}\`: ${removeNewlines(prop.description) || 'No description.'}${defaultStr}`);
+ }
+ lines.push('');
+ }
+
+ // CSS Parts
+ if (component.cssParts?.length > 0) {
+ lines.push('**CSS Parts:**');
+ lines.push('');
+ for (const part of component.cssParts) {
+ lines.push(`- \`${part.name}\`: ${removeNewlines(part.description) || 'No description.'}`);
+ }
+ lines.push('');
+ }
+
+ // CSS States
+ if (component.cssStates?.length > 0) {
+ lines.push('**CSS States:**');
+ lines.push('');
+ for (const state of component.cssStates) {
+ lines.push(`- \`${state.name}\`: ${removeNewlines(state.description) || 'No description.'}`);
+ }
+ lines.push('');
+ }
+
+ return lines;
+}
+
+/**
+ * Generates the complete llms.txt content.
+ */
+function generateLlmsTxt({ components, packageData, frontMatterCache, baseUrl }) {
+ const lines = [];
+
+ // H1 Title (required by llmstxt.org spec)
+ lines.push('# Web Awesome');
+ lines.push('');
+
+ // Blockquote summary
+ lines.push(`> ${packageData.description} Version ${packageData.version}.`);
+ lines.push('');
+
+ // Overview section
+ lines.push(
+ `
+Web Awesome provides a comprehensive set of customizable, accessible web components for building modern
+web applications. All components use shadow DOM and are framework-agnostic, working with vanilla JavaScript
+or any framework including React, Vue, Angular, and Svelte.
+
+Form controls are form-associated custom elements that work with native form validation and the
+Constraint Validation API.
+
+Font Awesome is the default icon library, so \`\` values should reference Font Awesome
+icon names.
+`.trim(),
+ );
+ lines.push('');
+
+ //
+ // Documentation
+ //
+ lines.push('## Documentation');
+ lines.push('');
+ lines.push(`For comprehensive documentation, visit ${baseUrl}/docs/`);
+ lines.push('');
+ lines.push(`- [Getting Started](${baseUrl}/docs/getting-started): Installation and setup guide`);
+ lines.push(`- [Components Overview](${baseUrl}/docs/components): Complete component reference`);
+ lines.push(`- [Theming](${baseUrl}/docs/theming): Customization and design tokens`);
+ lines.push(`- [Form Controls](${baseUrl}/docs/form-controls): Form integration and validation`);
+ lines.push('');
+
+ //
+ // Components
+ //
+ lines.push('## Components');
+ lines.push('');
+
+ const sortedComponentsList = [...components].sort((a, b) => a.tagName.localeCompare(b.tagName));
+
+ for (const component of sortedComponentsList) {
+ const frontMatter = frontMatterCache.get(component.tagName);
+ const description = removeNewlines(frontMatter?.description || component.summary || '');
+ const componentSlug = component.tagName.replace(/^wa-/, '');
+ const title = frontMatter?.title || componentSlug;
+
+ lines.push(
+ `- [${title}](${baseUrl}/docs/components/${componentSlug}): ${description || 'No description available.'}`,
+ );
+ }
+ lines.push('');
+
+ //
+ // Optional
+ //
+ lines.push('## Optional');
+ lines.push('');
+ lines.push(
+ `The following is a quick reference describing every component's API. For comprehensive documentation, refer to the component documentation using the URLs provided above.`,
+ );
+ lines.push('');
+
+ // Sort components alphabetically by tag name for the API reference
+ const sortedComponents = [...components].sort((a, b) => a.tagName.localeCompare(b.tagName));
+
+ for (const component of sortedComponents) {
+ lines.push(...generateComponentApiSection(component, frontMatterCache, baseUrl));
+ }
+
+ return lines.join('\n').trim();
+}
+
+/**
+ * A CEM plugin that generates an llms.txt file following the llmstxt.org specification.
+ */
+export function llmsTxtPlugin(options = {}) {
+ const {
+ outdir = 'dist-cdn',
+ docsDir = path.resolve(__dirname, '../docs'),
+ baseUrl = 'https://webawesome.com',
+ } = options;
+
+ return {
+ name: 'wa-llms-txt',
+ packageLinkPhase({ customElementsManifest }) {
+ const components = getAllComponents(customElementsManifest);
+ const packageData = customElementsManifest.package || {};
+ const frontMatterCache = loadAllFrontMatter(components, docsDir);
+
+ const llmsTxt = generateLlmsTxt({
+ components,
+ packageData,
+ frontMatterCache,
+ baseUrl,
+ });
+
+ // Write to the output directory
+ const outputPath = path.join(outdir, 'llms.txt');
+ fs.writeFileSync(outputPath, llmsTxt, 'utf-8');
+ },
+ };
+}
+
+export default llmsTxtPlugin;