diff --git a/.gitignore b/.gitignore
index bc614fc0ec5..b00f6dd876a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -71,6 +71,7 @@ public/**/**/nextImageExportOptimizer/
public/next-image-export-optimizer-hashes.json
src/directory/directory.json
src/directory/flatDirectory.json
+public/ai/
src/references/raw-references.json
.yarn/*
diff --git a/next-env.d.ts b/next-env.d.ts
index a4a7b3f5cfa..ddb76465e52 100644
--- a/next-env.d.ts
+++ b/next-env.d.ts
@@ -1,5 +1,6 @@
///
///
+import "./client/www/next-build/dev/types/routes.d.ts";
// NOTE: This file should not be edited
-// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
+// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
diff --git a/package.json b/package.json
index 5fbdba2acd3..92325bced47 100644
--- a/package.json
+++ b/package.json
@@ -122,7 +122,7 @@
"next-start": "next start --webpack",
"prepare": "husky install",
"analyze": "ANALYZE=true yarn next-build",
- "prebuild": "node src/directory/generateDirectory.mjs && node src/directory/generateFlatDirectory.mjs",
+ "prebuild": "node src/directory/generateDirectory.mjs && node src/directory/generateFlatDirectory.mjs && node tasks/generate-llms-txt.mjs",
"lint": "next lint --webpack",
"clean-references": "node tasks/clean-references.mjs"
},
diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx
index 3ff54bd60f6..c6a06f63240 100644
--- a/src/components/Layout/Layout.tsx
+++ b/src/components/Layout/Layout.tsx
@@ -40,6 +40,7 @@ import { Gen1Banner } from '@/components/Gen1Banner';
import { PinpointEOLBanner } from '@/components/PinpointEOLBanner';
import { LexV1EOLBanner } from '../LexV1EOLBanner';
import { ApiModalProvider } from '../ApiDocs/ApiModalProvider';
+import { MarkdownMenu } from '@/components/MarkdownMenu';
export const Layout = ({
children,
@@ -283,7 +284,19 @@ export const Layout = ({
) : null}
{shouldShowAIBanner ? : null}
{useCustomTitle ? null : (
- {pageTitle}
+
+ {pageTitle}
+
+
)}
{(isGen1GettingStarted || isGen1HowAmplifyWorks) && (
diff --git a/src/components/MarkdownMenu/MarkdownMenu.tsx b/src/components/MarkdownMenu/MarkdownMenu.tsx
new file mode 100644
index 00000000000..91325be9b09
--- /dev/null
+++ b/src/components/MarkdownMenu/MarkdownMenu.tsx
@@ -0,0 +1,194 @@
+import { useState, useRef, useEffect, useCallback } from 'react';
+import { Button, Flex, Text } from '@aws-amplify/ui-react';
+
+interface MarkdownMenuProps {
+ route: string;
+ isGen1?: boolean;
+ isHome?: boolean;
+ isOverview?: boolean;
+}
+
+function getMarkdownUrl(route: string): string {
+ // Strip platform prefix and trailing slash
+ // e.g. /react/build-a-backend/auth/set-up-auth/ → build-a-backend/auth/set-up-auth
+ const parts = route.replace(/^\//, '').replace(/\/$/, '').split('/');
+ const withoutPlatform = parts.slice(1).join('/');
+ return `/ai/pages/${withoutPlatform}.md`;
+}
+
+export function MarkdownMenu({ route, isGen1, isHome, isOverview }: MarkdownMenuProps) {
+ const [isOpen, setIsOpen] = useState(false);
+ const [copied, setCopied] = useState(false);
+ const menuRef = useRef(null);
+ const copiedTimerRef = useRef | null>(null);
+
+ const shouldRender = !isGen1 && !isHome && !isOverview;
+ const mdUrl = getMarkdownUrl(route);
+
+ const handleCopy = useCallback(async () => {
+ try {
+ const response = await fetch(mdUrl);
+ if (!response.ok) return;
+ const text = await response.text();
+ // Guard against accidentally copying HTML (e.g. 404 page)
+ if (/^\s* setCopied(false), 2000);
+ } catch {
+ // Silently fail if clipboard not available
+ }
+ }, [mdUrl]);
+
+ const handleOpenMd = useCallback(() => {
+ window.open(mdUrl, '_blank');
+ setIsOpen(false);
+ }, [mdUrl]);
+
+ useEffect(() => {
+ return () => {
+ if (copiedTimerRef.current) clearTimeout(copiedTimerRef.current);
+ };
+ }, []);
+
+ useEffect(() => {
+ function handleClickOutside(event: MouseEvent) {
+ if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
+ setIsOpen(false);
+ }
+ }
+ if (isOpen) {
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }
+ }, [isOpen]);
+
+ const handleKeyDown = useCallback(
+ (event: React.KeyboardEvent) => {
+ if (event.key === 'Escape') {
+ setIsOpen(false);
+ }
+ if (!isOpen) return;
+ const items = menuRef.current?.querySelectorAll(
+ '[role="menuitem"]'
+ );
+ if (!items?.length) return;
+ const focused = document.activeElement;
+ const index = Array.from(items).indexOf(focused as HTMLButtonElement);
+ if (event.key === 'ArrowDown') {
+ event.preventDefault();
+ items[index < items.length - 1 ? index + 1 : 0].focus();
+ } else if (event.key === 'ArrowUp') {
+ event.preventDefault();
+ items[index > 0 ? index - 1 : items.length - 1].focus();
+ }
+ },
+ [isOpen]
+ );
+
+ if (!shouldRender) return null;
+
+ return (
+
+
+ {isOpen && (
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/components/MarkdownMenu/__tests__/MarkdownMenu.test.tsx b/src/components/MarkdownMenu/__tests__/MarkdownMenu.test.tsx
new file mode 100644
index 00000000000..6b4e314aaaa
--- /dev/null
+++ b/src/components/MarkdownMenu/__tests__/MarkdownMenu.test.tsx
@@ -0,0 +1,149 @@
+import * as React from 'react';
+import { render, screen, waitFor, act } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { MarkdownMenu } from '../MarkdownMenu';
+
+// Mock fetch and clipboard
+const mockFetch = jest.fn();
+const mockWriteText = jest.fn();
+const mockOpen = jest.fn();
+
+beforeEach(() => {
+ global.fetch = mockFetch;
+ Object.assign(navigator, {
+ clipboard: { writeText: mockWriteText }
+ });
+ window.open = mockOpen;
+ mockFetch.mockReset();
+ mockWriteText.mockReset();
+ mockOpen.mockReset();
+});
+
+describe('MarkdownMenu', () => {
+ it('should render button with "Use with AI" label', () => {
+ render(
+
+ );
+ expect(screen.getByText('Use with AI')).toBeInTheDocument();
+ });
+
+ it('should not render on Gen1 pages', () => {
+ const { container } = render(
+
+ );
+ expect(container.innerHTML).toBe('');
+ });
+
+ it('should not render on home page', () => {
+ const { container } = render(
+
+ );
+ expect(container.innerHTML).toBe('');
+ });
+
+ it('should show dropdown menu items on click', async () => {
+ render(
+
+ );
+
+ await act(async () => {
+ userEvent.click(screen.getByText('Use with AI'));
+ });
+
+ expect(screen.getByText('Copy page MD')).toBeVisible();
+ expect(screen.getByText('Open this page as MD')).toBeVisible();
+ });
+
+ it('should copy markdown to clipboard when "Copy page MD" is clicked', async () => {
+ const mdContent = '# Test Page\n\nSome content.';
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ text: () => Promise.resolve(mdContent)
+ });
+ mockWriteText.mockResolvedValueOnce(undefined);
+
+ render(
+
+ );
+
+ await act(async () => {
+ userEvent.click(screen.getByText('Use with AI'));
+ });
+ await act(async () => {
+ userEvent.click(screen.getByText('Copy page MD'));
+ });
+
+ await waitFor(() => {
+ expect(mockFetch).toHaveBeenCalledWith(
+ '/ai/pages/build-a-backend/auth/set-up-auth.md'
+ );
+ });
+ expect(mockWriteText).toHaveBeenCalledWith(mdContent);
+ });
+
+ it('should open markdown file in new tab when "Open this page as MD" is clicked', async () => {
+ render(
+
+ );
+
+ await act(async () => {
+ userEvent.click(screen.getByText('Use with AI'));
+ });
+ await act(async () => {
+ userEvent.click(screen.getByText('Open this page as MD'));
+ });
+
+ expect(mockOpen).toHaveBeenCalledWith(
+ '/ai/pages/build-a-backend/auth/set-up-auth.md',
+ '_blank'
+ );
+ });
+
+ it('should construct correct markdown URL from route', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ text: () => Promise.resolve('content')
+ });
+ mockWriteText.mockResolvedValueOnce(undefined);
+
+ render(
+
+ );
+
+ await act(async () => {
+ userEvent.click(screen.getByText('Use with AI'));
+ });
+ await act(async () => {
+ userEvent.click(screen.getByText('Copy page MD'));
+ });
+
+ await waitFor(() => {
+ expect(mockFetch).toHaveBeenCalledWith(
+ '/ai/pages/build-a-backend/auth/set-up-auth.md'
+ );
+ });
+ });
+
+ it('should show "Copied!" feedback after copy', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ text: () => Promise.resolve('content')
+ });
+ mockWriteText.mockResolvedValueOnce(undefined);
+
+ render(
+
+ );
+
+ await act(async () => {
+ userEvent.click(screen.getByText('Use with AI'));
+ });
+ await act(async () => {
+ userEvent.click(screen.getByText('Copy page MD'));
+ });
+
+ await waitFor(() => {
+ expect(screen.getByText('Copied!')).toBeInTheDocument();
+ });
+ });
+});
diff --git a/src/components/MarkdownMenu/index.ts b/src/components/MarkdownMenu/index.ts
new file mode 100644
index 00000000000..daceaa3ef68
--- /dev/null
+++ b/src/components/MarkdownMenu/index.ts
@@ -0,0 +1 @@
+export { MarkdownMenu } from './MarkdownMenu';
diff --git a/src/styles/markdown-menu.scss b/src/styles/markdown-menu.scss
new file mode 100644
index 00000000000..d14e70212d2
--- /dev/null
+++ b/src/styles/markdown-menu.scss
@@ -0,0 +1,55 @@
+.markdown-menu {
+ position: relative;
+ flex-shrink: 0;
+
+ &__button {
+ padding: var(--amplify-space-xxs) var(--amplify-space-small);
+ border: 1px solid var(--amplify-colors-border-primary);
+ border-radius: var(--amplify-radii-small);
+ background: var(--amplify-colors-background-primary);
+ color: var(--amplify-colors-font-secondary);
+ font-size: var(--amplify-font-sizes-small);
+ cursor: pointer;
+
+ &:hover {
+ background: var(--amplify-colors-overlay-5);
+ color: var(--amplify-colors-font-primary);
+ }
+ }
+
+ &__chevron {
+ transition: transform 0.15s ease;
+ }
+
+ &__dropdown {
+ position: absolute;
+ right: 0;
+ top: calc(100% + var(--amplify-space-xxs));
+ background: var(--amplify-colors-background-primary);
+ border: 1px solid var(--amplify-colors-border-primary);
+ border-radius: var(--amplify-radii-small);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ z-index: 10;
+ min-width: 200px;
+ padding: var(--amplify-space-xxs) 0;
+ }
+
+ &__item {
+ display: flex;
+ align-items: center;
+ gap: var(--amplify-space-small);
+ width: 100%;
+ padding: var(--amplify-space-xs) var(--amplify-space-medium);
+ border: none;
+ background: none;
+ color: var(--amplify-colors-font-primary);
+ font-size: var(--amplify-font-sizes-small);
+ cursor: pointer;
+ text-align: left;
+ white-space: nowrap;
+
+ &:hover {
+ background: var(--amplify-colors-overlay-5);
+ }
+ }
+}
diff --git a/src/styles/styles.scss b/src/styles/styles.scss
index c6958471e7b..4e85db13d8b 100644
--- a/src/styles/styles.scss
+++ b/src/styles/styles.scss
@@ -25,6 +25,7 @@
@import './icons.scss';
@import './layout.scss';
@import './link-card.scss';
+@import './markdown-menu.scss';
@import './menu.scss';
@import './modal.scss';
@import './next-previous.scss';
diff --git a/tasks/__tests__/generate-llms-txt.test.ts b/tasks/__tests__/generate-llms-txt.test.ts
new file mode 100644
index 00000000000..c09bcdee4e6
--- /dev/null
+++ b/tasks/__tests__/generate-llms-txt.test.ts
@@ -0,0 +1,273 @@
+import {
+ collectGen2Pages,
+ transformMdxToMarkdown,
+ generateFrontmatter,
+ generateLlmsIndex
+} from '../generate-llms-txt.mjs';
+
+describe('generate-llms-txt', () => {
+ describe('collectGen2Pages', () => {
+ it('should return only Gen2 pages', () => {
+ const mockDirectory = {
+ path: 'src/pages/[platform]/index.tsx',
+ children: [
+ {
+ path: 'src/pages/[platform]/build-a-backend/auth/set-up-auth/index.mdx',
+ title: 'Set up Amplify Auth',
+ description: 'Learn how to set up auth.',
+ platforms: ['react', 'nextjs'],
+ lastUpdated: '2025-01-15T00:00:00.000Z',
+ route: '/[platform]/build-a-backend/auth/set-up-auth'
+ },
+ {
+ path: 'src/pages/gen1/[platform]/start/index.mdx',
+ title: 'Gen1 Start',
+ description: 'Gen1 getting started.',
+ platforms: ['react'],
+ route: '/gen1/[platform]/start'
+ }
+ ]
+ };
+
+ const pages = collectGen2Pages(mockDirectory);
+ expect(pages.length).toBe(1);
+ expect(pages[0].title).toBe('Set up Amplify Auth');
+ });
+
+ it('should skip external links', () => {
+ const mockDirectory = {
+ path: 'src/pages/[platform]/index.tsx',
+ children: [
+ {
+ path: 'src/pages/[platform]/build-a-backend/auth/index.mdx',
+ title: 'Auth',
+ route: '/[platform]/build-a-backend/auth',
+ platforms: ['react'],
+ isExternal: true
+ }
+ ]
+ };
+
+ const pages = collectGen2Pages(mockDirectory);
+ expect(pages.length).toBe(0);
+ });
+
+ it('should extract correct section from route', () => {
+ const mockDirectory = {
+ path: 'src/pages/[platform]/index.tsx',
+ children: [
+ {
+ path: 'src/pages/[platform]/build-a-backend/auth/set-up-auth/index.mdx',
+ title: 'Set up Auth',
+ description: '',
+ platforms: ['react'],
+ route: '/[platform]/build-a-backend/auth/set-up-auth',
+ lastUpdated: ''
+ }
+ ]
+ };
+
+ const pages = collectGen2Pages(mockDirectory);
+ expect(pages[0].section).toBe('build-a-backend/auth');
+ });
+ });
+
+ describe('transformMdxToMarkdown', () => {
+ it('should strip exports and imports', () => {
+ const input = `import { getCustomStaticPath } from '@/utils/getCustomStaticPath';
+
+export const meta = {
+ title: 'Test',
+ description: 'test page',
+ platforms: ['react']
+};
+
+export function getStaticPaths() {
+ return getCustomStaticPath(meta.platforms);
+};
+
+export function getStaticProps() {
+ return {
+ props: {
+ meta
+ }
+ };
+};
+
+# Hello World
+
+Some content here.`;
+
+ const result = transformMdxToMarkdown(input);
+ expect(result).not.toContain('export const meta');
+ expect(result).not.toContain('import');
+ expect(result).not.toContain('getStaticPaths');
+ expect(result).not.toContain('getStaticProps');
+ expect(result).toContain('# Hello World');
+ expect(result).toContain('Some content here.');
+ });
+
+ it('should convert InlineFilter to HTML comments', () => {
+ const input = `React only content`;
+ const result = transformMdxToMarkdown(input);
+ expect(result).toContain('');
+ expect(result).toContain('React only content');
+ expect(result).toContain('');
+ });
+
+ it('should convert Callout info', () => {
+ const input = `This is important info.`;
+ const result = transformMdxToMarkdown(input);
+ expect(result).toContain('> **Info:** This is important info.');
+ });
+
+ it('should convert Callout warning', () => {
+ const input = `Be careful!`;
+ const result = transformMdxToMarkdown(input);
+ expect(result).toContain('> **Warning:** Be careful!');
+ });
+
+ it('should convert BlockSwitcher/Block to headings', () => {
+ const input = `
+
+TypeScript content here.
+
+
+JavaScript content here.
+
+`;
+
+ const result = transformMdxToMarkdown(input);
+ expect(result).toContain('#### [TypeScript]');
+ expect(result).toContain('TypeScript content here.');
+ expect(result).toContain('#### [JavaScript]');
+ expect(result).toContain('JavaScript content here.');
+ expect(result).not.toContain('');
+ expect(result).not.toContain('');
+ });
+
+ it('should convert Accordion to details/summary', () => {
+ const input = `Some hidden content.`;
+ const result = transformMdxToMarkdown(input);
+ expect(result).toContain('Click to expand
');
+ expect(result).toContain('Some hidden content.');
+ expect(result).toContain(' ');
+ });
+
+ it('should strip Overview component', () => {
+ const input = `
+
+## Next section`;
+ const result = transformMdxToMarkdown(input);
+ expect(result).not.toContain('Overview');
+ expect(result).toContain('## Next section');
+ });
+
+ it('should preserve code blocks', () => {
+ const input = `Some text.
+
+\`\`\`tsx
+
+
+ This should not be transformed
+
+
+\`\`\`
+
+More text.`;
+
+ const result = transformMdxToMarkdown(input);
+ expect(result).toContain('');
+ expect(result).toContain('');
+ expect(result).toContain('This should not be transformed');
+ });
+
+ it('should strip JSX comments', () => {
+ const input = `Some text.
+{/* This is a comment */}
+More text.`;
+
+ const result = transformMdxToMarkdown(input);
+ expect(result).not.toContain('This is a comment');
+ expect(result).toContain('Some text.');
+ expect(result).toContain('More text.');
+ });
+
+ it('should handle nested InlineFilters', () => {
+ const input = `
+Outer content.
+Inner react only.
+`;
+
+ const result = transformMdxToMarkdown(input);
+ expect(result).toContain('');
+ expect(result).toContain('');
+ expect(result).toContain('Outer content.');
+ expect(result).toContain('Inner react only.');
+ });
+ });
+
+ describe('generateFrontmatter', () => {
+ it('should produce valid YAML frontmatter', () => {
+ const mockNode = {
+ title: 'Set up Amplify Auth',
+ description: 'Learn how to set up auth.',
+ platforms: ['react', 'nextjs'],
+ route: '/[platform]/build-a-backend/auth/set-up-auth',
+ path: 'src/pages/[platform]/build-a-backend/auth/set-up-auth/index.mdx',
+ lastUpdated: '2025-01-15T00:00:00.000Z',
+ section: 'build-a-backend/auth'
+ };
+
+ const result = generateFrontmatter(mockNode);
+ expect(result).toMatch(/^---/);
+ expect(result).toMatch(/---$/);
+ expect(result).toContain('title: "Set up Amplify Auth"');
+ expect(result).toContain('section: "build-a-backend/auth"');
+ expect(result).toContain('"react"');
+ expect(result).toContain('"nextjs"');
+ expect(result).toContain('gen: 2');
+ expect(result).toContain('last-updated: "2025-01-15T00:00:00.000Z"');
+ expect(result).toContain(
+ 'url: "https://docs.amplify.aws/react/build-a-backend/auth/set-up-auth/"'
+ );
+ });
+ });
+
+ describe('generateLlmsIndex', () => {
+ it('should group pages by section', () => {
+ const mockNodes = [
+ {
+ title: 'Set up Auth',
+ description: 'Auth setup guide.',
+ platforms: ['react'],
+ route: '/[platform]/build-a-backend/auth/set-up-auth',
+ section: 'build-a-backend'
+ },
+ {
+ title: 'Quickstart',
+ description: 'Get started quickly.',
+ platforms: ['react'],
+ route: '/[platform]/start/quickstart',
+ section: 'start'
+ },
+ {
+ title: 'Data Modeling',
+ description: 'Model your data.',
+ platforms: ['react'],
+ route: '/[platform]/build-a-backend/data/data-modeling',
+ section: 'build-a-backend'
+ }
+ ];
+
+ const result = generateLlmsIndex(mockNodes);
+ expect(result).toContain('## build-a-backend');
+ expect(result).toContain('## start');
+ expect(result).toContain('- [Set up Auth]');
+ expect(result).toContain('- [Quickstart]');
+ expect(result).toContain('- [Data Modeling]');
+ expect(result).toContain(': Auth setup guide.');
+ expect(result).toContain('Markdown: /ai/pages/');
+ });
+ });
+});
diff --git a/tasks/generate-llms-txt.mjs b/tasks/generate-llms-txt.mjs
new file mode 100644
index 00000000000..bb21a399f68
--- /dev/null
+++ b/tasks/generate-llms-txt.mjs
@@ -0,0 +1,406 @@
+import { promises as fs, readFileSync } from 'fs';
+import path from 'path';
+
+const AI_PATH = './public/ai';
+const PAGES_PATH = `${AI_PATH}/pages`;
+const DOMAIN = 'https://docs.amplify.aws';
+const PROJECT_ROOT = path.resolve('.');
+
+/**
+ * Recursively walk directory.json and collect Gen2 page nodes
+ * (routes starting with /[platform]/).
+ */
+export function collectGen2Pages(node, section = '') {
+ const pages = [];
+
+ if (!node) return pages;
+
+ const route = node.route || '';
+ const isGen2 = route.startsWith('/[platform]/');
+ const isExternal = node.isExternal === true;
+
+ if (isGen2 && !isExternal && node.title) {
+ // Extract section: everything between /[platform]/ and the last segment
+ const routeWithoutPlatform = route.replace('/[platform]/', '');
+ const parts = routeWithoutPlatform.split('/');
+ const pageSection = parts.length > 1 ? parts.slice(0, -1).join('/') : parts[0];
+
+ pages.push({
+ title: node.title,
+ description: node.description || '',
+ platforms: node.platforms || [],
+ route: node.route,
+ path: node.path,
+ lastUpdated: node.lastUpdated || '',
+ section: section || pageSection
+ });
+ }
+
+ if (node.children) {
+ // Determine section from this node if it's a Gen2 container
+ const currentSection = isGen2 && route
+ ? route.replace('/[platform]/', '').split('/')[0]
+ : section;
+
+ for (const child of node.children) {
+ const childSection = isGen2
+ ? route.replace('/[platform]/', '')
+ : currentSection;
+ pages.push(...collectGen2Pages(child, childSection));
+ }
+ }
+
+ return pages;
+}
+
+/**
+ * Strip an export block that starts with the given prefix, handling nested braces.
+ */
+function stripExportBlock(content, prefix) {
+ const idx = content.indexOf(prefix);
+ if (idx === -1) return content;
+
+ // Find the first opening brace after the prefix
+ const braceStart = content.indexOf('{', idx);
+ if (braceStart === -1) return content;
+
+ // Count braces to find the matching closing brace
+ let depth = 0;
+ let i = braceStart;
+ for (; i < content.length; i++) {
+ if (content[i] === '{') depth++;
+ else if (content[i] === '}') {
+ depth--;
+ if (depth === 0) break;
+ }
+ }
+
+ // Remove from prefix start to after the closing brace (and optional semicolons/whitespace)
+ let end = i + 1;
+ while (end < content.length && (content[end] === ';' || content[end] === '\n' || content[end] === '\r' || content[end] === ' ')) {
+ end++;
+ }
+
+ return content.slice(0, idx) + content.slice(end);
+}
+
+/**
+ * Transform raw MDX content into clean markdown.
+ */
+export function transformMdxToMarkdown(rawMdx, srcDir = './src') {
+ let content = rawMdx;
+
+ // Step 1: Extract fenced code blocks as placeholders
+ const codeBlocks = [];
+ content = content.replace(/```[\s\S]*?```/g, (match) => {
+ const index = codeBlocks.length;
+ codeBlocks.push(match);
+ return `__CODE_BLOCK_${index}__`;
+ });
+
+ // Step 2: Strip export blocks (meta, getStaticPaths, getStaticProps)
+ // Use brace-counting to handle nested braces
+ content = stripExportBlock(content, 'export const meta');
+ content = stripExportBlock(content, 'export function getStaticPaths');
+ content = stripExportBlock(content, 'export const getStaticPaths');
+ content = stripExportBlock(content, 'export function getStaticProps');
+ content = stripExportBlock(content, 'export const getStaticProps');
+ // Catch any remaining single-line export statements
+ content = content.replace(/^export\s+(const|function|default)\s+.*$/gm, '');
+
+ // Step 3: Resolve Fragments - capture imports and inline content
+ const fragmentImports = {};
+ const importRegex = /import\s+(\w+)\s+from\s+['"]([^'"]+\.mdx)['"]\s*;?/g;
+ let importMatch;
+ while ((importMatch = importRegex.exec(content)) !== null) {
+ fragmentImports[importMatch[1]] = importMatch[2];
+ }
+
+ // Replace with inlined content
+ content = content.replace(
+ //g,
+ (match) => {
+ // Extract the variable names from the fragments prop
+ const varNames = new Set();
+ const varRegex = /:\s*(\w+)/g;
+ let varMatch;
+ while ((varMatch = varRegex.exec(match)) !== null) {
+ varNames.add(varMatch[1]);
+ }
+
+ // Inline the first unique fragment (they're typically the same content for all platforms)
+ for (const varName of varNames) {
+ const fragmentPath = fragmentImports[varName];
+ if (fragmentPath) {
+ try {
+ const resolvedPath = path.resolve(srcDir, '..', fragmentPath);
+ // Ensure the resolved path stays within the project root
+ if (!resolvedPath.startsWith(PROJECT_ROOT)) {
+ continue;
+ }
+ const fragmentContent = readFileSync(resolvedPath, 'utf-8');
+ return transformMdxToMarkdown(fragmentContent, srcDir);
+ } catch {
+ // If we can't read the fragment, just remove the tag
+ }
+ }
+ }
+ return '';
+ }
+ );
+
+ // Step 4: Strip import statements
+ content = content.replace(/^import\s+.*?;\s*$/gm, '');
+ content = content.replace(/^import\s+.*?from\s+['"].*?['"]\s*$/gm, '');
+
+ // Step 5: Convert ...
+ // The regex matches innermost InlineFilter pairs first. Multiple passes handle
+ // nesting — each pass peels off one layer. We loop until no more replacements
+ // occur rather than using a fixed count.
+ let prevContent;
+ do {
+ prevContent = content;
+ content = content.replace(
+ /([\s\S]*?)<\/InlineFilter>/g,
+ (match, filters, inner) => {
+ // Clean up the filter list
+ const filterList = filters.replace(/[[\]'"]/g, '').trim();
+ return `\n${inner.trim()}\n`;
+ }
+ );
+ } while (content !== prevContent);
+
+ // Step 6: Convert / → #### [X]
+ content = content.replace(//g, '');
+ content = content.replace(/<\/BlockSwitcher>/g, '');
+ content = content.replace(//g, (match, name) => {
+ return `#### [${name}]`;
+ });
+ content = content.replace(/<\/Block>/g, '');
+
+ // Step 7: Convert → blockquote
+ content = content.replace(
+ /([\s\S]*?)<\/Callout>/g,
+ (match, type, inner) => {
+ const label = type === 'warning' ? 'Warning' : 'Info';
+ const lines = inner.trim().split('\n');
+ return lines.map((line, i) => {
+ if (i === 0) return `> **${label}:** ${line}`;
+ return `> ${line}`;
+ }).join('\n');
+ }
+ );
+
+ // Step 8: Convert →
+ content = content.replace(
+ /]*)?>/g,
+ '$1
'
+ );
+ content = content.replace(/<\/Accordion>/g, ' ');
+
+ // Step 9: Strip ,