Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions apps/docs/app/llms-full.txt/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { NextResponse } from "next/server";
import { generateLlmsFullTxt } from "@/lib/llms";

const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || "https://siwa.aptos.dev";

export async function GET() {
const content = generateLlmsFullTxt(BASE_URL);

return new NextResponse(content, {
headers: {
"Content-Type": "text/plain; charset=utf-8",
"Cache-Control": "public, max-age=3600, s-maxage=3600",
},
});
Comment on lines +7 to +14
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The GET handler doesn't include error handling for the case where generateLlmsFullTxt might fail (e.g., if the docs directory doesn't exist or there are file system errors). While readMdxFiles returns an empty array if the directory doesn't exist, file system errors during reading could still cause the handler to fail ungracefully.

Consider wrapping the content generation in a try-catch block and returning an appropriate error response if generation fails.

Suggested change
const content = generateLlmsFullTxt(BASE_URL);
return new NextResponse(content, {
headers: {
"Content-Type": "text/plain; charset=utf-8",
"Cache-Control": "public, max-age=3600, s-maxage=3600",
},
});
try {
const content = generateLlmsFullTxt(BASE_URL);
return new NextResponse(content, {
headers: {
"Content-Type": "text/plain; charset=utf-8",
"Cache-Control": "public, max-age=3600, s-maxage=3600",
},
});
} catch (error) {
// Optionally log the error for debugging/monitoring purposes
console.error("Failed to generate LLMS full text content:", error);
return NextResponse.json(
{ error: "Failed to generate content" },
{ status: 500 }
);
}

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +14
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The route handlers read and process all documentation files on every request. While caching headers are set (1 hour), the server-side processing happens on each request to the route, which could be inefficient for large documentation sites.

Consider implementing one of the following optimizations:

  1. Use Next.js static generation to pre-generate these files at build time
  2. Implement server-side caching to avoid re-reading files on every request
  3. Use Next.js revalidation mechanisms for better performance

This is especially important if the documentation grows significantly.

Copilot uses AI. Check for mistakes.
}
15 changes: 15 additions & 0 deletions apps/docs/app/llms.txt/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { NextResponse } from "next/server";
import { generateLlmsTxt } from "@/lib/llms";

const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || "https://siwa.aptos.dev";

export async function GET() {
const content = generateLlmsTxt(BASE_URL);

return new NextResponse(content, {
headers: {
"Content-Type": "text/plain; charset=utf-8",
"Cache-Control": "public, max-age=3600, s-maxage=3600",
},
});
Comment on lines +7 to +14
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The GET handler doesn't include error handling for the case where generateLlmsTxt might fail (e.g., if the docs directory doesn't exist or there are file system errors). While readMdxFiles returns an empty array if the directory doesn't exist, file system errors during reading could still cause the handler to fail ungracefully.

Consider wrapping the content generation in a try-catch block and returning an appropriate error response if generation fails.

Suggested change
const content = generateLlmsTxt(BASE_URL);
return new NextResponse(content, {
headers: {
"Content-Type": "text/plain; charset=utf-8",
"Cache-Control": "public, max-age=3600, s-maxage=3600",
},
});
try {
const content = generateLlmsTxt(BASE_URL);
return new NextResponse(content, {
headers: {
"Content-Type": "text/plain; charset=utf-8",
"Cache-Control": "public, max-age=3600, s-maxage=3600",
},
});
} catch (error) {
console.error("Failed to generate llms.txt content:", error);
return new NextResponse("Internal Server Error", {
status: 500,
headers: {
"Content-Type": "text/plain; charset=utf-8",
"Cache-Control": "no-store",
},
});
}

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +14
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The route handlers read and process all documentation files on every request. While caching headers are set (1 hour), the server-side processing happens on each request to the route, which could be inefficient for large documentation sites.

Consider implementing one of the following optimizations:

  1. Use Next.js static generation to pre-generate these files at build time
  2. Implement server-side caching to avoid re-reading files on every request
  3. Use Next.js revalidation mechanisms for better performance

This is especially important if the documentation grows significantly.

Copilot uses AI. Check for mistakes.
}
314 changes: 314 additions & 0 deletions apps/docs/lib/llms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,314 @@
import fs from "node:fs";
import path from "node:path";
import matter from "gray-matter";

interface DocPage {
slug: string;
title: string;
description?: string;
content: string;
section: string;
}

interface DocSection {
title: string;
pages: DocPage[];
}

const DOCS_DIR = path.join(process.cwd(), "app/docs");

/**
* JSX components to remove from content (with their children)
* Add new components here as they are used in documentation
*/
const JSX_COMPONENTS_TO_REMOVE = ["Steps", "TSDoc", "Callout"];

/**
* Extract frontmatter and content from MDX file using gray-matter
*/
function parseMdxFile(content: string): {
frontmatter: Record<string, string>;
body: string;
} {
try {
const parsed = matter(content);
// Convert frontmatter values to strings for consistency
const frontmatter: Record<string, string> = {};
for (const [key, value] of Object.entries(parsed.data)) {
frontmatter[key] = String(value);
}
return {
frontmatter,
body: parsed.content.trim(),
};
} catch {
// Fallback if gray-matter fails
return { frontmatter: {}, body: content };
}
}

/**
* Extract title from MDX content (frontmatter or first heading)
*/
function extractTitle(
frontmatter: Record<string, string>,
body: string,
): string {
if (frontmatter.title) {
return frontmatter.title;
}

const headingMatch = body.match(/^#\s+(.+)$/m);
if (headingMatch) {
return headingMatch[1];
}

return "Untitled";
}

/**
* Extract description from content (first paragraph after imports/heading)
*/
function extractDescription(body: string): string | undefined {
// Remove imports and JSX components
const cleanedBody = body
.replace(/^import\s+.*$/gm, "")
.replace(/<[^>]+\/>/g, "")
.replace(/<[^>]+>[\s\S]*?<\/[^>]+>/g, "")
.trim();

// Find first paragraph after heading
const lines = cleanedBody.split(/\r?\n/).filter((line) => line.trim());
for (const line of lines) {
const trimmed = line.trim();
if (
trimmed &&
!trimmed.startsWith("#") &&
!trimmed.startsWith("```") &&
!trimmed.startsWith("<") &&
!trimmed.startsWith("import")
) {
return trimmed.slice(0, 200);
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description is truncated at exactly 200 characters using slice(0, 200), which could cut a word in the middle, resulting in incomplete words in the description. This may look unprofessional in the generated LLMS.txt files.

Consider truncating at word boundaries instead, or adding an ellipsis when the description is truncated. For example, you could find the last space before the 200-character limit and truncate there.

Suggested change
return trimmed.slice(0, 200);
if (trimmed.length <= 200) {
return trimmed;
}
const maxLength = 200;
const lastSpaceBeforeLimit = trimmed.lastIndexOf(" ", maxLength);
const cutPosition =
lastSpaceBeforeLimit > 0 ? lastSpaceBeforeLimit : maxLength;
const truncated = trimmed.slice(0, cutPosition).trimEnd();
return truncated + "...";

Copilot uses AI. Check for mistakes.
}
}

return undefined;
}

/**
* Clean MDX content for plain text output
*/
function cleanMdxContent(content: string): string {
// Build regex patterns from the component list
const componentsPattern = JSX_COMPONENTS_TO_REMOVE.join("|");

return (
content
// Remove import statements
.replace(/^import\s+.*$/gm, "")
// Remove JSX/TSX components with their content
.replace(
new RegExp(`<(${componentsPattern})[^>]*>[\\s\\S]*?<\\/\\1>`, "g"),
"",
)
Comment on lines +99 to +113
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex pattern for removing JSX components with content uses a greedy [\s\S]*? pattern which could potentially fail to properly match nested components with the same name. For example, if there's a <Steps> component containing another <Steps> component, the regex may not correctly identify the closing tag.

Consider using a more robust parsing approach or documenting this limitation. For simple documentation content this may be acceptable, but it's a potential edge case to be aware of.

Suggested change
* Clean MDX content for plain text output
*/
function cleanMdxContent(content: string): string {
// Build regex patterns from the component list
const componentsPattern = JSX_COMPONENTS_TO_REMOVE.join("|");
return (
content
// Remove import statements
.replace(/^import\s+.*$/gm, "")
// Remove JSX/TSX components with their content
.replace(
new RegExp(`<(${componentsPattern})[^>]*>[\\s\\S]*?<\\/\\1>`, "g"),
"",
)
* Remove specified JSX components and their children from MDX content.
* This handles nested components with the same name using a simple depth counter.
*/
function removeJsxComponentsWithContent(
source: string,
componentNames: string[],
): string {
let result = source;
for (const name of componentNames) {
const openTag = `<${name}`;
const closeTag = `</${name}>`;
let searchFrom = 0;
while (true) {
const firstOpen = result.indexOf(openTag, searchFrom);
if (firstOpen === -1) break;
let depth = 0;
let i = firstOpen;
while (i < result.length) {
const nextOpen = result.indexOf(openTag, i);
const nextClose = result.indexOf(closeTag, i);
if (nextClose === -1 && nextOpen === -1) {
// No further matching tags; abort to avoid infinite loop for this component.
i = -1;
break;
}
if (nextOpen !== -1 && (nextOpen < nextClose || nextClose === -1)) {
// Potential opening tag.
const endOfOpen = result.indexOf(">", nextOpen);
if (endOfOpen === -1) {
i = -1;
break;
}
const selfClosingPos = result.lastIndexOf("/>", endOfOpen);
const isSelfClosing =
selfClosingPos !== -1 &&
selfClosingPos >= nextOpen &&
selfClosingPos <= endOfOpen;
if (!isSelfClosing) {
depth += 1;
}
i = endOfOpen + 1;
} else {
// Closing tag for this component.
if (depth === 0) {
// Malformed structure; break to avoid infinite loop.
i = -1;
break;
}
depth -= 1;
const endOfClose = nextClose + closeTag.length;
i = endOfClose;
if (depth === 0) {
// Remove from the first opening tag to the end of this closing tag.
result =
result.slice(0, firstOpen) + result.slice(endOfClose);
// Continue searching from the same position in the updated string.
searchFrom = firstOpen;
break;
}
}
}
if (i === -1) {
// Could not find a complete, well-formed pair; stop processing this component.
break;
}
}
}
return result;
}
/**
* Clean MDX content for plain text output
*/
function cleanMdxContent(content: string): string {
// Build regex patterns from the component list
const componentsPattern = JSX_COMPONENTS_TO_REMOVE.join("|");
const contentWithoutImports = content.replace(/^import\s+.*$/gm, "");
const withoutComponents = removeJsxComponentsWithContent(
contentWithoutImports,
JSX_COMPONENTS_TO_REMOVE,
);
return (
withoutComponents
// Remove self-closing JSX/TSX components for the specified list

Copilot uses AI. Check for mistakes.
.replace(new RegExp(`<(${componentsPattern})[^>]*\\/>`, "g"), "")
// Remove self-closing JSX tags
.replace(/<[A-Z][a-zA-Z]*[^>]*\/>/g, "")
// Remove JSX component wrappers but keep content
.replace(/<\/?[A-Z][a-zA-Z]*[^>]*>/g, "")
// Remove HTML tags
.replace(/<\/?[a-z][a-zA-Z]*[^>]*>/g, "")
// Clean up extra newlines
.replace(/\n{3,}/g, "\n\n")
.trim()
);
}

/**
* Determine section from path parts
*/
function getSectionFromPath(pathParts: string[]): string {
if (pathParts.length === 0 || pathParts[0] === "") {
return "Overview";
}

if (pathParts[0] === "ts-aptos-labs-siwa") {
return pathParts[1] === "reference"
? "@aptos-labs/siwa API Reference"
: "@aptos-labs/siwa";
}

if (pathParts[0] === "ts-aptos-labs-wallet-adapter-react") {
return "@aptos-labs/wallet-adapter-react";
}

if (pathParts[0] === "ts-aptos-labs-wallet-standard") {
return "@aptos-labs/wallet-standard";
}

if (pathParts[0] === "wallet-integrations") {
return "Support";
}

return "Overview";
}

/**
* Recursively read all MDX files from a directory
*/
function readMdxFiles(dir: string, basePath = ""): DocPage[] {
const pages: DocPage[] = [];

if (!fs.existsSync(dir)) {
return pages;
}

const entries = fs.readdirSync(dir, { withFileTypes: true });

for (const entry of entries) {
const fullPath = path.join(dir, entry.name);

if (entry.isDirectory()) {
// Skip _meta files directory
if (entry.name.startsWith("_")) continue;

const newBasePath = basePath ? `${basePath}/${entry.name}` : entry.name;
pages.push(...readMdxFiles(fullPath, newBasePath));
} else if (entry.name === "page.mdx") {
const content = fs.readFileSync(fullPath, "utf-8");
const { frontmatter, body } = parseMdxFile(content);
const title = extractTitle(frontmatter, body);
const description = extractDescription(body);

// Determine section from path - handle empty basePath correctly
const pathParts = basePath ? basePath.split("/") : [];
const section = getSectionFromPath(pathParts);

pages.push({
slug: basePath || "index",
title,
description,
content: cleanMdxContent(body),
section,
});
}
}

return pages;
}

/**
* Get all documentation pages organized by section
*/
export function getDocPages(): DocSection[] {
const pages = readMdxFiles(DOCS_DIR);

// Group by section
const sectionMap = new Map<string, DocPage[]>();

for (const page of pages) {
const existing = sectionMap.get(page.section) || [];
existing.push(page);
sectionMap.set(page.section, existing);
}

// Define section order
const sectionOrder = [
"Overview",
"@aptos-labs/siwa",
"@aptos-labs/siwa API Reference",
"@aptos-labs/wallet-adapter-react",
"@aptos-labs/wallet-standard",
"Support",
];

const sections: DocSection[] = [];

for (const title of sectionOrder) {
const sectionPages = sectionMap.get(title);
if (sectionPages && sectionPages.length > 0) {
sections.push({ title, pages: sectionPages });
}
}
Comment on lines +227 to +232
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function only includes sections that are explicitly listed in the sectionOrder array (lines 216-223). If new documentation sections are added that don't match these exact names, they will be silently excluded from the generated LLMS.txt files.

Consider either:

  1. Adding a catch-all at the end to include any remaining sections from sectionMap that weren't in sectionOrder, or
  2. Adding a warning/logging when sections are being excluded

This ensures all documentation is included even when new sections are added.

Copilot uses AI. Check for mistakes.

return sections;
}

/**
* Generate llms.txt content (overview with links)
*/
export function generateLlmsTxt(baseUrl: string): string {
const sections = getDocPages();

let output = `# Sign in with Aptos (SIWA)

> Authenticate users securely using their Aptos account. A standardized authentication protocol for Aptos accounts that replaces the traditional connect + signMessage flow with a streamlined one-click signIn method.

The "Sign in with Aptos" (SIWA) standard introduces a secure and user-friendly way for users to authenticate to off-chain resources by proving ownership of their Aptos account. SIWA leverages Aptos accounts to avoid reliance on traditional schemes like SSO while incorporating security measures to combat phishing attacks and improve user visibility.

`;

for (const section of sections) {
output += `## ${section.title}\n\n`;

for (const page of section.pages) {
const url = page.slug === "index" ? "/docs" : `/docs/${page.slug}`;
const description = page.description ? `: ${page.description}` : "";
output += `- [${page.title}](${url})${description}\n`;
}

output += "\n";
}

output += `## External Resources

- [AIP-116](https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-116.md): Aptos Improvement Proposal for SIWA
- [GitHub Repository](https://github.com/aptos-labs/sign-in-with-aptos): Source code and examples
- [Wallet Adapter Documentation](https://aptos.dev/en/build/sdks/wallet-adapter/dapp): Official Aptos wallet adapter docs

## Optional: llms-full.txt

For a more comprehensive version with full page content, see [llms-full.txt](${baseUrl}/llms-full.txt)
`;

return output;
}

/**
* Generate llms-full.txt content (full documentation)
*/
export function generateLlmsFullTxt(baseUrl: string): string {
const sections = getDocPages();

let output = `# Sign in with Aptos (SIWA) - Full Documentation

> Authenticate users securely using their Aptos account. A standardized authentication protocol for Aptos accounts.

Source: ${baseUrl}
GitHub: https://github.com/aptos-labs/sign-in-with-aptos

---

`;

for (const section of sections) {
output += `## ${section.title}\n\n`;

for (const page of section.pages) {
output += `### ${page.title}\n\n`;
output += `${page.content}\n\n`;
output += "---\n\n";
}
}

output += `## External Resources

- AIP-116: https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-116.md
- GitHub Repository: https://github.com/aptos-labs/sign-in-with-aptos
- Wallet Adapter Documentation: https://aptos.dev/en/build/sdks/wallet-adapter/dapp
- EIP-4361: https://eips.ethereum.org/EIPS/eip-4361
- CAIP-122: https://chainagnostic.org/CAIPs/caip-122
`;

return output;
}
1 change: 1 addition & 0 deletions apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@tanstack/react-query": "^5.83.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"gray-matter": "^4.0.3",
"lucide-react": "^0.468.0",
"motion": "^12.23.5",
"next": "15.3.6",
Expand Down
6 changes: 5 additions & 1 deletion apps/docs/public/robots.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@ Allow: /

# Disallow crawling of certain paths
Disallow: /api/
Disallow: /_next/
Disallow: /_next/

# LLMS.txt - Documentation for Large Language Models
# https://llmstxt.org/
# LLMS.txt available at: https://siwa.aptos.dev/llms.txt
Loading
Loading