, , etc.)
+ // Only remove tags that start with uppercase (React components), not lowercase HTML tags
+ content = content.replace(/<[A-Z][a-zA-Z0-9]*[^>]*\/>/g, "");
+ content = content.replace(/<[A-Z][a-zA-Z0-9]*[^>]*>/g, "");
+ content = content.replace(/<\/[A-Z][a-zA-Z0-9]*>/g, "");
+
+ // Remove JSX attributes from remaining tags
+ content = content.replace(/<([a-z][a-z0-9]*)\s+[^>]*>/g, "<$1>");
+
+ // Remove standalone HTML blocks (not in code blocks)
+ // These are leftover HTML structures that aren't useful for LLM consumption
+ content = removeStandaloneHTML(content);
+
+ return content;
+}
+
+function removeStandaloneHTML(content: string): string {
+ // First, protect code blocks by replacing them with placeholders
+ let codeBlockPlaceholders: string[] = [];
+ let placeholderIndex = 0;
+
+ content = content.replace(/```[\s\S]*?```/g, (match) => {
+ let placeholder = `__CODE_BLOCK_PLACEHOLDER_${placeholderIndex}__`;
+ codeBlockPlaceholders[placeholderIndex] = match;
+ placeholderIndex++;
+ return placeholder;
+ });
+
+ // Remove nested HTML structures that span multiple lines
+ // These are leftover HTML blocks that aren't in code blocks
+ let lines = content.split("\n");
+ let result: string[] = [];
+ let inHTMLBlock = false;
+ let htmlBlockDepth = 0;
+
+ for (let i = 0; i < lines.length; i++) {
+ let line = lines[i];
+ let trimmed = line.trim();
+
+ let startsWithOpeningTag = /^\s*<[a-z][a-z0-9]*[^>]*>\s*$/.test(trimmed);
+ let startsWithClosingTag = /^\s*<\/[a-z][a-z0-9]*>\s*$/.test(trimmed);
+ let isSelfClosingTag = /^\s*<[a-z][a-z0-9]*[^>]*\/>\s*$/.test(trimmed);
+ let hasOpeningTag = /<[a-z][a-z0-9]*[^>]*>/.test(trimmed);
+ let hasClosingTag = /<\/[a-z][a-z0-9]*>/.test(trimmed);
+ let isCompleteHTMLElement = /^\s*<[a-z][a-z0-9]*[^>]*>.*<\/[a-z][a-z0-9]*>\s*$/.test(trimmed) || isSelfClosingTag;
+
+ if (isSelfClosingTag && !inHTMLBlock) {
+ continue;
+ } else if (isCompleteHTMLElement && !inHTMLBlock) {
+ continue;
+ } else if (startsWithOpeningTag && !inHTMLBlock) {
+ inHTMLBlock = true;
+ htmlBlockDepth = 1;
+ } else if (hasOpeningTag && inHTMLBlock && !hasClosingTag) {
+ htmlBlockDepth++;
+ } else if (startsWithClosingTag && inHTMLBlock) {
+ htmlBlockDepth--;
+ if (htmlBlockDepth === 0) {
+ inHTMLBlock = false;
+ continue;
+ }
+ } else if (inHTMLBlock) {
+ continue;
+ } else {
+ result.push(line);
+ }
+ }
+
+ content = result.join("\n");
+
+ // Restore code blocks
+ for (let i = 0; i < codeBlockPlaceholders.length; i++) {
+ content = content.replace(`__CODE_BLOCK_PLACEHOLDER_${i}__`, codeBlockPlaceholders[i]);
+ }
+
+ return content;
+}
+
+function removeCodeDirectives(content: string): string {
+ // Remove [!code ...] directives
+ // These can appear as comments in various formats
+ content = content.replace(//g, "");
+ content = content.replace(/\/\*\s*\[!code[^\]]+\]\s*\*\//g, "");
+ content = content.replace(/#\s*\[!code[^\]]+\]/g, "");
+ content = content.replace(/\/\/\s*\[!code[^\]]+\]/g, "");
+
+ // Remove prettier-ignore comments
+ content = content.replace(//g, "");
+ content = content.replace(/\/\*\s*prettier-ignore\s*\*\//g, "");
+ content = content.replace(/#\s*prettier-ignore/g, "");
+ content = content.replace(/\/\/\s*prettier-ignore/g, "");
+
+ return content;
+}
+
+function cleanWhitespace(content: string): string {
+ // Remove excessive blank lines (more than 2 consecutive)
+ content = content.replace(/\n{3,}/g, "\n\n");
+
+ // Trim each line
+ content = content
+ .split("\n")
+ .map((line) => line.trimEnd())
+ .join("\n");
+
+ // Remove leading/trailing whitespace
+ return content.trim();
+}
diff --git a/src/app/llms.txt/route.ts b/src/app/llms.txt/route.ts
new file mode 100644
index 000000000..3dd31136f
--- /dev/null
+++ b/src/app/llms.txt/route.ts
@@ -0,0 +1,132 @@
+import { NextResponse } from "next/server";
+import fs from "node:fs/promises";
+import path from "node:path";
+import { getDocPageSlugs } from "../(docs)/docs/api";
+import { extractTextFromMDX } from "../api/llms-txt/extract-text";
+import index from "../(docs)/docs/index";
+
+export const dynamic = "force-static";
+export const revalidate = false;
+
+export async function GET() {
+ let output = "# Tailwind CSS Documentation\n\n";
+ output +=
+ "This file contains a concatenated, text-only version of all Tailwind CSS documentation pages, optimized for Large Language Model consumption.\n\n";
+ output += "---\n\n";
+
+ let slugs = await getDocPageSlugs();
+
+ // Build a map of slugs to their section and title from the index
+ let slugToSection = new Map();
+ for (let [section, entries] of Object.entries(index)) {
+ for (let entry of entries) {
+ let [title, docPath] = entry;
+ let slug = docPath.replace("/docs/", "");
+ slugToSection.set(slug, { section, title });
+
+ // Handle nested children
+ if (entry.length > 2 && Array.isArray(entry[2])) {
+ for (let [childTitle, childPath] of entry[2]) {
+ let childSlug = childPath.replace("/docs/", "");
+ slugToSection.set(childSlug, { section, title: childTitle });
+ }
+ }
+ }
+ }
+
+ // Process each slug in the order defined by the index
+ let processedSlugs = new Set();
+ let currentSection = "";
+
+ for (let [section, entries] of Object.entries(index)) {
+ if (section !== currentSection) {
+ if (currentSection !== "") {
+ output += "\n";
+ }
+ output += `## ${section}\n\n`;
+ currentSection = section;
+ }
+
+ for (let entry of entries) {
+ let [title, docPath] = entry;
+ let slug = docPath.replace("/docs/", "");
+
+ if (processedSlugs.has(slug)) continue;
+ processedSlugs.add(slug);
+
+ output += await processSlug(slug, title);
+
+ // Handle nested children
+ if (entry.length > 2 && Array.isArray(entry[2])) {
+ for (let [childTitle, childPath] of entry[2]) {
+ let childSlug = childPath.replace("/docs/", "");
+ if (processedSlugs.has(childSlug)) continue;
+ processedSlugs.add(childSlug);
+ output += await processSlug(childSlug, childTitle);
+ }
+ }
+ }
+ }
+
+ // Process any remaining slugs that weren't in the index
+ for (let slug of slugs) {
+ if (!processedSlugs.has(slug)) {
+ let sectionInfo = slugToSection.get(slug);
+ let title = sectionInfo?.title || slug;
+ output += await processSlug(slug, title);
+ }
+ }
+
+ return new NextResponse(output, {
+ headers: {
+ "Content-Type": "text/plain; charset=utf-8",
+ "Cache-Control": process.env.NODE_ENV === "development" ? "no-cache" : "public, max-age=3600",
+ },
+ });
+}
+
+async function processSlug(slug: string, title: string): Promise {
+ try {
+ let filePath = path.join(process.cwd(), "./src/docs", `${slug}.mdx`);
+ let content = await fs.readFile(filePath, "utf8");
+
+ // Extract title and description from exports
+ let titleMatch = content.match(/export\s+const\s+title\s*=\s*["']([^"']+)["']/);
+ let descriptionMatch = content.match(/export\s+const\s+description\s*=\s*["']([^"']+)["']/);
+
+ let pageTitle = titleMatch ? titleMatch[1] : title;
+ let description = descriptionMatch ? descriptionMatch[1] : "";
+
+ // Extract text from MDX
+ let extractedText = extractTextFromMDX(content);
+
+ // Remove the title/description header that extractTextFromMDX adds (we'll format it ourselves)
+ if (extractedText.startsWith("# ")) {
+ let lines = extractedText.split("\n");
+ // Skip the title line, description line(s), and separator
+ let startIndex = 0;
+ for (let i = 0; i < lines.length; i++) {
+ if (lines[i].startsWith("---")) {
+ startIndex = i + 1;
+ break;
+ }
+ }
+ extractedText = lines.slice(startIndex).join("\n").trim();
+ }
+
+ // Format the page
+ let pageOutput = `### ${pageTitle}\n\n`;
+ if (description) {
+ pageOutput += `${description}\n\n`;
+ }
+ pageOutput += `URL: /docs/${slug}\n\n`;
+ pageOutput += `${extractedText}\n\n`;
+ pageOutput += "---\n\n";
+
+ return pageOutput;
+ } catch (error) {
+ // Skip files that can't be read
+ console.error(`Error processing ${slug}:`, error);
+ return "";
+ }
+}