import { defineNuxtModule, logger } from '@nuxt/kit' import { resolve } from 'node:path' import { readFile, writeFile } from 'node:fs/promises' const log = logger.withTag('docus') type I18nLocale = string | { code: string } type DocusI18nOptions = { locales?: I18nLocale[] } export default defineNuxtModule({ meta: { name: 'markdown-rewrite', }, setup(_options, nuxt) { nuxt.hooks.hook('nitro:init', (nitro) => { if (nitro.options.dev || !nitro.options.preset.includes('vercel')) { return } nitro.hooks.hook('compiled', async () => { const vcJSON = resolve(nitro.options.output.dir, 'config.json') const vcConfig = JSON.parse(await readFile(vcJSON, 'utf8')) // Check if llms.txt exists before setting up any routes let llmsTxt const llmsTxtPath = resolve(nitro.options.output.publicDir, 'llms.txt') try { llmsTxt = await readFile(llmsTxtPath, 'utf-8') } catch { log.warn('llms.txt not found, skipping markdown redirect routes') return } // Always redirect / to /llms.txt and ensure plain text content type const markdownHeaders = { 'content-type': 'text/markdown; charset=utf-8', } const routes = [ { src: '^/$', dest: '/llms.txt', headers: markdownHeaders, has: [{ type: 'header', key: 'accept', value: '(.*)text/markdown(.*)' }], }, { src: '^/$', dest: '/llms.txt', headers: markdownHeaders, has: [{ type: 'header', key: 'user-agent', value: 'curl/.*' }], }, ] // Check if i18n is enabled const i18nOptions = (nuxt.options as typeof nuxt.options & { i18n?: DocusI18nOptions }).i18n const isI18nEnabled = !!i18nOptions?.locales let localeCodes: string[] = [] if (isI18nEnabled) { // Get locale codes const locales = i18nOptions?.locales || [] localeCodes = locales.map((locale: I18nLocale) => { return typeof locale === 'string' ? locale : locale.code }) // Create a regex pattern for all locales (e.g., "en|fr|es") const localePattern = localeCodes.join('|') // Add routes for each locale homepage: /{locale} → /llms.txt routes.push( { src: `^/(${localePattern})$`, dest: '/llms.txt', headers: markdownHeaders, has: [{ type: 'header', key: 'accept', value: '(.*)text/markdown(.*)' }], }, { src: `^/(${localePattern})$`, dest: '/llms.txt', headers: markdownHeaders, has: [{ type: 'header', key: 'user-agent', value: 'curl/.*' }], }, ) } // Parse llms.txt to get all documentation pages const urlRegex = /\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g const matches = [...llmsTxt.matchAll(urlRegex)] for (const match of matches) { const url = match[2] if (!url) continue try { // Extract path from URL const urlObj = new URL(url) const rawPath = urlObj.pathname // Skip root path (already handled) if (rawPath === '/') continue // Only process raw markdown URLs from llms.txt if (!rawPath.startsWith('/raw/')) continue // Convert /raw/en/getting-started/installation.md to /en/getting-started/installation const pagePath = rawPath.replace('/raw', '').replace(/\.md$/, '') // Skip locale homepages (e.g., /en, /fr) - they already redirect to /llms.txt if (isI18nEnabled) { const isLocaleHomepage = localeCodes.some(code => pagePath === `/${code}`) if (isLocaleHomepage) continue } // Add redirect routes: page URL → raw markdown URL const docsRoutes = [ { src: `^${pagePath}$`, dest: rawPath, headers: markdownHeaders, has: [{ type: 'header', key: 'accept', value: '(.*)text/markdown(.*)' }], }, { src: `^${pagePath}$`, dest: rawPath, headers: markdownHeaders, has: [{ type: 'header', key: 'user-agent', value: 'curl/.*' }], }, ] routes.push(...docsRoutes) } catch { // Skip invalid URLs } } vcConfig.routes.unshift(...routes) await writeFile(vcJSON, JSON.stringify(vcConfig, null, 2), 'utf8') log.info(`Successfully wrote ${routes.length} routes to ${vcJSON} (serve markdown content to AI agents)`) }) }) }, })