Files
supabase/apps/docs/scripts/llms.ts
T
Pamela Chia 2a9555d459 feat(docs,www): make supabase.com machine-readable for LLM agents (#44670)
## Summary

Makes supabase.com machine-readable for LLM agents and AI crawlers. Adds
a combined `llms-full.txt` (product overview + docs), curated product
overview `.txt` files, auto-generated pricing content, and updates the
`llms.txt` index to reference everything.

**Why:** supabase.com had `llms.txt` pointing to per-SDK doc files, but
no `llms-full.txt` (every competitor has one), no machine-readable
marketing content, and no pricing data agents could parse. Agents
evaluating Supabase got docs but no product overview or pricing, leading
to inaccurate comparisons.

## What's new

### `llms-full.txt` (auto-generated, combines www + docs)
- Product Overview section (~500 lines): homepage, all 6 products,
pricing
- Documentation section (~122K lines): guides, SDK references, CLI
reference
- 4.4MB total, regenerated on every docs deploy
- Reads product `.txt` files from `apps/www/public/llms/` at build time

### Product overview `.txt` files (`apps/www/public/llms/`)
- Curated summaries: homepage, database, auth, storage, edge functions,
realtime, vector
- `pricing.txt` is auto-generated from `packages/shared-data` (plans,
pricing, compute add-ons) via `generateLlmsPricing.mjs`, includes full
feature comparison matrix
- Other product files are hand-maintained (these pages change ~1x/year
per git history)
- Reminder comments added to all 7 marketing page source files

### `llms.txt` index (auto-generated)
- Two sections: Documentation (existing SDK/guide links) and Product
Overview (marketing page links)
- Links to `llms-full.txt` for bulk ingestion

### Rewrite changes
- Added `/llms-full.txt` rewrite to docs app
- Scoped `/llms/*.txt` wildcard to only match docs source slugs (guides,
js, dart, etc.), so marketing `.txt` files in `www/public/llms/` are
served directly

## Changes

- `apps/docs/scripts/llms.ts`: generate `llms-full.txt` combining www
product content + docs, update `llms.txt` index with marketing links
- `apps/www/lib/rewrites.js`: add `llms-full.txt` rewrite, scope docs
proxy to known slugs
- `apps/www/public/llms/*.txt`: 8 product overview files (7
hand-curated, 1 auto-generated)
- `apps/www/scripts/generateLlmsPricing.mjs`: build script generating
pricing.txt from shared-data; uses `getPlanValue()` guard in
`buildAddOnsSection` and `buildFeatureComparisonSection` to handle
missing plan keys defensively
- `apps/www/package.json`: add pricing generation to content:build
- `apps/www/pages/*.tsx`: reminder comments for LLM content updates

## Testing

Tested locally:
- [x] `pnpm run build:llms` generates combined `llms-full.txt` (4.4MB)
with Product Overview + Documentation sections
- [x] `llms.txt` index has Documentation + Product Overview sections
with `llms-full.txt` reference
- [x] `pricing.txt` auto-generated from shared-data with correct plan
tiers, compute add-ons, disk pricing, and feature comparison
- [x] Scoped rewrite regex matches docs slugs but not marketing slugs
- [x] Marketing `.txt` files served from `public/`

Post-deploy verification:
- [ ] `curl https://supabase.com/llms-full.txt` returns combined product
+ docs content
- [ ] `curl https://supabase.com/llms/database.txt` returns product
overview (not proxied to docs)
- [ ] `curl https://supabase.com/llms/guides.txt` still proxies to docs
app
- [ ] `curl https://supabase.com/llms/pricing.txt` returns
auto-generated pricing

## Maintenance

| Content | Auto-updates? | Trigger |
|---------|--------------|---------|
| `llms-full.txt` | Yes | Every docs deploy |
| `llms.txt` index | Yes | Every docs deploy |
| `pricing.txt` | Yes | Every www build (reads from shared-data) |
| Product `.txt` files (7) | No | ~1x/year, reminder comments in source
pages |

## Linear

- fixes GROWTH-758

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Documentation**
* Added comprehensive AI-friendly product overviews (Auth, Database,
Edge Functions, Realtime, Storage, Vector) and a detailed pricing
document with plan comparisons, add-ons, disk tiers, and feature tables
* Added a consolidated "full" markdown output that combines curated
product overview content with per-source documentation

* **Chores**
* Build now generates the richer documentation outputs and pricing
automatically
* Improved routing so only scoped documentation .txt assets are proxied
while others are served directly
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-15 00:48:35 +09:00

235 lines
6.5 KiB
TypeScript

import './utils/dotenv.js'
import 'dotenv/config'
import fs from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { isFeatureEnabled } from '../../../packages/common/enabled-features/index.js'
import { getCustomContent } from '../lib/custom-content/getCustomContent.js'
import {
fetchCliLibReferenceSource,
fetchCSharpLibReferenceSource,
fetchDartLibReferenceSource,
fetchGuideSources,
fetchJsLibReferenceSource,
fetchKtLibReferenceSource,
fetchPythonLibReferenceSource,
fetchSwiftLibReferenceSource,
type SearchSource,
} from './search/sources/index.js'
interface Source {
title: string
/**
* Path relative to https://supabase.com. No leading slash
*/
relPath: string
fetch: () => Promise<SearchSource[]>
enabled: boolean
}
const {
sdkCsharp: sdkCsharpEnabled,
sdkDart: sdkDartEnabled,
sdkKotlin: sdkKotlinEnabled,
sdkPython: sdkPythonEnabled,
sdkSwift: sdkSwiftEnabled,
} = isFeatureEnabled(['sdk:csharp', 'sdk:dart', 'sdk:kotlin', 'sdk:python', 'sdk:swift'])
const { metadataTitle } = getCustomContent(['metadata:title'])
function toLink(source: Source) {
return `[${source.title}](https://supabase.com/${source.relPath})`
}
const SOURCES: Source[] = [
{
title: 'Supabase Guides',
relPath: 'llms/guides.txt',
fetch: fetchGuideSources,
enabled: true,
},
{
title: 'Supabase Reference (JavaScript)',
relPath: 'llms/js.txt',
fetch: async () =>
(await fetchJsLibReferenceSource()).filter(
(item): item is SearchSource => item !== undefined
),
enabled: true,
},
{
title: 'Supabase Reference (Dart)',
relPath: 'llms/dart.txt',
fetch: async () =>
(await fetchDartLibReferenceSource()).filter(
(item): item is SearchSource => item !== undefined
),
enabled: sdkDartEnabled,
},
{
title: 'Supabase Reference (Swift)',
relPath: 'llms/swift.txt',
fetch: async () =>
(await fetchSwiftLibReferenceSource()).filter(
(item): item is SearchSource => item !== undefined
),
enabled: sdkSwiftEnabled,
},
{
title: 'Supabase Reference (Kotlin)',
relPath: 'llms/kotlin.txt',
fetch: async () =>
(await fetchKtLibReferenceSource()).filter(
(item): item is SearchSource => item !== undefined
),
enabled: sdkKotlinEnabled,
},
{
title: 'Supabase Reference (Python)',
relPath: 'llms/python.txt',
fetch: async () =>
(await fetchPythonLibReferenceSource()).filter(
(item): item is SearchSource => item !== undefined
),
enabled: sdkPythonEnabled,
},
{
title: 'Supabase Reference (C#)',
relPath: 'llms/csharp.txt',
fetch: async () =>
(await fetchCSharpLibReferenceSource()).filter(
(item): item is SearchSource => item !== undefined
),
enabled: sdkCsharpEnabled,
},
{
title: 'Supabase CLI Reference',
relPath: 'llms/cli.txt',
fetch: async () =>
(await fetchCliLibReferenceSource()).filter(
(item): item is SearchSource => item !== undefined
),
enabled: true,
},
]
// Product overview .txt files are hand-curated and live in apps/www/public/llms/.
// These links always point to production since llms.txt is only meaningful in prod.
const PRODUCT_OVERVIEW_LINKS = [
'- [Supabase Overview](https://supabase.com/llms/homepage.txt)',
'- [Supabase Database](https://supabase.com/llms/database.txt)',
'- [Supabase Auth](https://supabase.com/llms/auth.txt)',
'- [Supabase Storage](https://supabase.com/llms/storage.txt)',
'- [Supabase Edge Functions](https://supabase.com/llms/edge-functions.txt)',
'- [Supabase Realtime](https://supabase.com/llms/realtime.txt)',
'- [Supabase Vector](https://supabase.com/llms/vector.txt)',
'- [Supabase Pricing](https://supabase.com/llms/pricing.txt)',
].join('\n')
async function generateMainLlmsTxt() {
const sourceLinks = SOURCES.filter((source) => source.enabled !== false)
.map((source) => `- ${toLink(source)}`)
.join('\n')
const fullText = [
`# ${metadataTitle}`,
'',
'For the complete documentation in a single file, see [Full Documentation](https://supabase.com/llms-full.txt).',
'',
'## Documentation',
'',
sourceLinks,
'',
'## Product Overview',
'',
PRODUCT_OVERVIEW_LINKS,
].join('\n')
await fs.writeFile('public/llms.txt', fullText)
}
// Product overview .txt files live in apps/www/public/llms/, read at build time.
// Order matters: homepage first, pricing last, products alphabetical in between.
const PRODUCT_LLM_FILES = [
'homepage.txt',
'auth.txt',
'database.txt',
'edge-functions.txt',
'realtime.txt',
'storage.txt',
'vector.txt',
'pricing.txt',
]
const PRODUCT_LLMS_DIR = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
'../../../apps/www/public/llms'
)
async function readProductLlmContent(): Promise<string> {
const contents = await Promise.all(
PRODUCT_LLM_FILES.map((file) => {
const filePath = path.join(PRODUCT_LLMS_DIR, file)
return fs.readFile(filePath, 'utf-8')
})
)
return contents.join('\n\n---\n\n')
}
async function generateLlmsTxt() {
try {
await fs.mkdir('public/llms', { recursive: true })
const enabledSources = SOURCES.filter((source) => source.enabled !== false)
// Fetch all sources once, reuse for both per-SDK files and llms-full.txt
const [productContent, ...fetchedSources] = await Promise.all([
readProductLlmContent(),
...enabledSources.map(async (sourceDefn) => {
const source = await sourceDefn.fetch()
const sourceText = source
.map((section) => {
section.process()
return section.extractIndexedContent()
})
.join('\n\n')
return { defn: sourceDefn, text: sourceText }
}),
])
await Promise.all([
generateMainLlmsTxt(),
// Per-SDK files
...fetchedSources.map(({ defn, text }) =>
fs.writeFile(`public/${defn.relPath}`, `${defn.title}\n\n${text}`)
),
// Combined file: product overview + all docs
fs.writeFile(
'public/llms-full.txt',
[
'# Supabase',
'',
'## Product Overview',
'',
productContent,
'',
'---',
'',
'## Documentation',
'',
fetchedSources.map(({ defn, text }) => `# ${defn.title}\n\n${text}`).join('\n\n---\n\n'),
].join('\n')
),
])
} catch (err) {
console.error(err)
throw err
}
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
generateLlmsTxt()
}