]*slot[^>]*>/g, '')
+ .replace(/<\/div>/g, '')
+ .replace(/
/g, '')
+ .replace(/<\/h5>/g, '')
+ // Remove multiple newlines
+ .replace(/\n{3,}/g, '\n\n')
+ .trim()
+}
+
+/**
+ * Extracts frontmatter from markdown content
+ * @param {string} content - Raw markdown content
+ * @returns {Object} Frontmatter data
+ */
+function extractFrontmatter (content) {
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/)
+ if (!frontmatterMatch) return {}
+
+ const frontmatter = {}
+ const lines = frontmatterMatch[1].split('\n')
+ for (const line of lines) {
+ const [ key, ...valueParts ] = line.split(':')
+ if (key && valueParts.length) {
+ frontmatter[key.trim()] = valueParts.join(':').trim()
+ }
+ }
+ return frontmatter
+}
+
+/**
+ * Generates a URL from a relative file path
+ * @param {string} relativePath - Relative path to the markdown file
+ * @returns {string} Full URL
+ */
+function filePathToUrl (relativePath) {
+ const urlPath = relativePath
+ .replace(/\.md$/, '')
+ .replace(/:/g, '') // Remove colons from path (e.g., :tutorials)
+ return `${BASE_URL}/docs/en/${urlPath}`
+}
+
+/**
+ * Processes a markdown file and extracts its content
+ * @param {string} filePath - Path to the markdown file
+ * @param {string} docsDir - Base docs directory
+ * @returns {string} Processed content with metadata
+ */
+function processMarkdownFile (filePath, docsDir) {
+ const content = readFileSync(filePath, 'utf-8')
+ const relativePath = relative(docsDir, filePath)
+ const frontmatter = extractFrontmatter(content)
+ const cleanContent = cleanMarkdownContent(content)
+
+ const metadata = [
+ frontmatter.title ? `# ${frontmatter.title}` : null,
+ `Source: ${filePathToUrl(relativePath)}`,
+ frontmatter.description ? `Description: ${frontmatter.description}` : null,
+ frontmatter.category ? `Category: ${frontmatter.category}` : null,
+ ]
+ .filter(Boolean)
+ .join('\n')
+
+ return `${metadata}\n\n${cleanContent}\n`
+}
+
+/**
+ * Recursively processes all markdown files in a directory
+ * @param {string} dir - Directory to process
+ * @param {string} docsDir - Base docs directory for relative paths
+ * @returns {string[]} Array of processed file contents
+ */
+function processDirectory (dir, docsDir) {
+ const results = []
+
+ if (!existsSync(dir)) {
+ console.error(`Directory does not exist: ${dir}`)
+ return results
+ }
+
+ let files
+ try {
+ files = readdirSync(dir, { withFileTypes: true })
+ }
+ catch (err) {
+ console.error(`Error reading directory ${dir}:`, err.message)
+ return results
+ }
+
+ for (const file of files) {
+ if (config.excludes.includes(file.name)) continue
+
+ const fullPath = join(dir, file.name)
+
+ if (file.isDirectory()) {
+ results.push(...processDirectory(fullPath, docsDir))
+ }
+ else if (file.name.endsWith('.md')) {
+ try {
+ results.push(processMarkdownFile(fullPath, docsDir))
+ }
+ catch (err) {
+ console.error(`Error processing ${fullPath}:`, err.message)
+ }
+ }
+ }
+
+ return results
+}
+
+async function _handler () {
+ // Try local dev path first (src/views), then fall back to production symlink (node_modules/@architect/views)
+ let docsDir = join(__dirname, '..', '..', 'views', 'docs', 'en')
+
+ if (!existsSync(docsDir)) {
+ docsDir = join(__dirname, 'node_modules', '@architect', 'views', 'docs', 'en')
+ }
+
+ console.log('Attempting to read docs from:', docsDir)
+
+ const header = `# Architect (arc.codes) - Complete Documentation
+
+> This is the complete documentation for Architect, a simple framework for building and delivering powerful Functional Web Apps (FWAs) on AWS.
+
+> For a high-level overview, see: ${BASE_URL}/llms.txt
+
+---
+
+`
+
+ const content = processDirectory(docsDir, docsDir)
+ const separator = '\n\n---\n\n'
+ const body = header + content.join(separator)
+
+ return {
+ statusCode: 200,
+ headers: {
+ 'content-type': 'text/plain; charset=utf-8',
+ 'cache-control': 'max-age=86400',
+ },
+ body,
+ }
+}
+
+export const handler = arc.http.async(_handler)
diff --git a/src/http/get-llms_txt/index.mjs b/src/http/get-llms_txt/index.mjs
new file mode 100644
index 00000000..c9d455b1
--- /dev/null
+++ b/src/http/get-llms_txt/index.mjs
@@ -0,0 +1,84 @@
+import toc from '../../views/docs/table-of-contents.mjs'
+
+const BASE_URL = 'https://arc.codes'
+
+/**
+ * Generates a URL for a documentation page
+ * @param {string[]} pathParts - Path segments
+ * @returns {string} Full URL
+ */
+function docUrl (pathParts) {
+ const slug = pathParts
+ .map(part => part.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''))
+ .join('/')
+ return `${BASE_URL}/docs/en/${slug}`
+}
+
+/**
+ * Recursively generates markdown links from the table of contents structure
+ * @param {Array} items - TOC items (strings or objects)
+ * @param {string[]} parentPath - Parent path segments
+ * @param {number} depth - Current nesting depth
+ * @returns {string} Markdown formatted links
+ */
+function generateLinks (items, parentPath = [], depth = 0) {
+ const indent = ' '.repeat(depth)
+ const lines = []
+
+ for (const item of items) {
+ if (typeof item === 'string') {
+ // Simple string item - it's a doc page
+ const path = [ ...parentPath, item ]
+ lines.push(`${indent}- [${item}](${docUrl(path)})`)
+ }
+ else if (typeof item === 'object' && !Array.isArray(item)) {
+ // Object with nested structure
+ for (const [ key, value ] of Object.entries(item)) {
+ if (Array.isArray(value)) {
+ // Category with sub-items
+ lines.push(`${indent}- ${key}`)
+ lines.push(generateLinks(value, [ ...parentPath, key ], depth + 1))
+ }
+ }
+ }
+ }
+
+ return lines.join('\n')
+}
+
+export async function handler () {
+ const sections = []
+
+ sections.push('# Architect (arc.codes)')
+ sections.push('')
+ sections.push('> Architect is a simple framework for building and delivering powerful Functional Web Apps (FWAs) on AWS')
+ sections.push('')
+ sections.push('## Documentation')
+ sections.push('')
+
+ for (const [ sectionName, items ] of Object.entries(toc)) {
+ sections.push(`### ${sectionName}`)
+ sections.push('')
+ sections.push(generateLinks(items, [ sectionName ]))
+ sections.push('')
+ }
+
+ // Add quick links section
+ sections.push('## Quick Links')
+ sections.push('')
+ sections.push(`- [GitHub Repository](https://github.com/architect/architect)`)
+ sections.push(`- [Full Documentation for LLMs](${BASE_URL}/llms-full.txt)`)
+ sections.push(`- [Discord Community](https://discord.gg/y5A2eTsCRX)`)
+ sections.push('')
+
+ const content = sections.join('\n')
+
+ return {
+ statusCode: 200,
+ headers: {
+ 'content-type': 'text/plain; charset=utf-8',
+ 'cache-control': 'max-age=86400',
+ },
+ body: content,
+ }
+}
diff --git a/src/views/modules/components/copy-markdown.mjs b/src/views/modules/components/copy-markdown.mjs
new file mode 100644
index 00000000..d6cc1c61
--- /dev/null
+++ b/src/views/modules/components/copy-markdown.mjs
@@ -0,0 +1,26 @@
+export default function CopyMarkdown (state = {}) {
+ const { markdown } = state
+
+ if (!markdown) return ''
+
+ // Escape the markdown for safe embedding in a data attribute
+ const escapedMarkdown = markdown
+ .replace(/&/g, '&')
+ .replace(/"/g, '"')
+ .replace(//g, '>')
+
+ return `
+
+`
+}
+
diff --git a/src/views/modules/components/edit-link.mjs b/src/views/modules/components/edit-link.mjs
index 370869c8..399b7353 100644
--- a/src/views/modules/components/edit-link.mjs
+++ b/src/views/modules/components/edit-link.mjs
@@ -1,8 +1,6 @@
export default function EditLink (state = {}) {
const { editURL } = state
return editURL ? `
-
+Edit this doc on GitHub →
` : ''
}
diff --git a/src/views/modules/document/html.mjs b/src/views/modules/document/html.mjs
index 9ce8b706..d17967a1 100644
--- a/src/views/modules/document/html.mjs
+++ b/src/views/modules/document/html.mjs
@@ -1,4 +1,5 @@
import Banner from '../components/banner.mjs'
+import CopyMarkdown from '../components/copy-markdown.mjs'
import DocumentOutline from '../components/document-outline.mjs'
import EditLink from '../components/edit-link.mjs'
import GoogleAnalytics from './ga.mjs'
@@ -14,6 +15,7 @@ export default function HTML (props = {}) {
html = '',
editURL = '',
lang = 'en',
+ markdown = '',
scripts = '',
slug = '',
state = {},
@@ -75,9 +77,14 @@ ${Symbols}
>
${title}
+
+ ${CopyMarkdown({ markdown })}
+
${html}
- ${EditLink({ editURL })}
+
+ ${EditLink({ editURL })}
+
${DocumentOutline(props)}
diff --git a/test/backend/redirect-map-test.mjs b/test/backend/redirect-map-test.mjs
index 8e110107..4b927c08 100644
--- a/test/backend/redirect-map-test.mjs
+++ b/test/backend/redirect-map-test.mjs
@@ -12,8 +12,8 @@ test('redirect map middleware', async t => {
http: {
method: 'GET',
path: '/examples',
- }
- }
+ },
+ },
})
const expectedResponse = {
statusCode: 301,
@@ -27,9 +27,9 @@ test('redirect map middleware', async t => {
requestContext: {
http: {
method: 'get',
- path: '/unmapped/path'
- }
- }
+ path: '/unmapped/path',
+ },
+ },
})
assert.ok(!nonRedirectResponse, "Don't respond to unmapped path")
@@ -37,9 +37,9 @@ test('redirect map middleware', async t => {
requestContext: {
http: {
method: 'POST',
- path: '/examples'
- }
- }
+ path: '/examples',
+ },
+ },
})
assert.ok(!postResponse, "Don't respond to POST method")
})
diff --git a/test/frontend/sidebar-test.mjs b/test/frontend/sidebar-test.mjs
index 80ecb189..2c3c1146 100644
--- a/test/frontend/sidebar-test.mjs
+++ b/test/frontend/sidebar-test.mjs
@@ -31,14 +31,14 @@ function Item (state = {}) {
? Heading3({
children: Anchor({
children: child,
- href: slugify(child)
+ href: slugify(child),
}),
- depth
+ depth,
})
: ''
}
${children}
- `
+ `,
})
}
@@ -69,26 +69,26 @@ const map = {
item: Li,
headings: [
Heading3,
- Heading4
- ]
+ Heading4,
+ ],
}
test('render object to list', t => {
const map = {
list: Ul,
- item: Li
+ item: Li,
}
const data = {
'one': [
'a',
'b',
- 'c'
+ 'c',
],
'two': [
'd',
'e',
- 'f'
- ]
+ 'f',
+ ],
}
const expected = `