Contexto
La documentación está duplicada y desincronizada entre dos sitios:
nan/ (este repo, landing) — fuente de verdad real, contenido correcto y actualizado.
nan-discord-bot/ — sirve documentación desde bot/docs/knowledge/, una copia obsoleta y parcial (3 archivos vs 7 páginas en la landing).
Ejemplo de deriva ya en producción:
- Bot
intro.md: "solicita tu API Key al Staff".
- Landing
index.astro: "genera tu API Key desde la sección API Keys de la plataforma".
Objetivo: la landing como fuente única, con un contrato HTTP que el bot (y cualquier futuro cliente) consume sin parsear presentación.
Solución
Migrar el contenido de src/pages/docs/*.astro a Astro Content Collections en src/content/docs/*.md. Exponer dos endpoints públicos en el mismo Worker:
GET /api/docs/manifest.json — índice con slug, título, hash SHA-256 y URL de cada archivo.
GET /api/docs/{slug}.md — Markdown crudo con frontmatter.
El bot hace pull periódico al manifest, compara hashes con su SQLite (lógica ya existente) y solo re-embebe lo que cambió.
Flujo de datos
┌──────────────────────────────────────────┐
│ nan/ (este repo) │
│ │
│ src/content/docs/ │
│ ├── intro.md ◄── fuente │
│ ├── getting-started.md única │
│ ├── api.md de verdad │
│ ├── models.md │
│ ├── examples.md │
│ ├── agents.md │
│ └── apps.md │
│ │
│ editor → git commit → PR → merge a main│
└──────────────────┬───────────────────────┘
│ wrangler deploy (CI)
▼
┌─────────────────────────────────────────────────┐
│ Cloudflare Worker (nan.builders) │
│ │
│ /docs/[slug] HTML (Docs.astro) │
│ /api/docs/manifest.json JSON índice │
│ /api/docs/{slug}.md Markdown crudo │
│ │
│ Cache API: ETag/Last-Modified por archivo │
└─────────┬───────────────────────────┬───────────┘
│ │
navegador │ │ HTTPS poll (15 min)
▼ ▼
usuario web ┌─────────────────────┐
│ nan-discord-bot │
│ │
│ DocsClient │
│ - fetch manifest │
│ - diff vs hashes │
│ - fetch .md changed│
│ - re-embed │
│ SimpleVectorStore │
└─────────────────────┘
Contratos HTTP
GET /api/docs/manifest.json
GET /api/docs/{slug}.md
Content-Type: text/markdown; charset=utf-8
ETag: "sha256:9f2a...c4"
Last-Modified: Wed, 20 May 2026 09:15:00 GMT
Cache-Control: public, max-age=60, s-maxage=300
---
title: Bienvenido a NaN
description: ...
order: 0
---
# Bienvenido a NaN
(cuerpo Markdown plano)
Reglas:
- Solo Markdown estándar en el cuerpo.
contentHash es del cuerpo (sin frontmatter): cambios cosméticos de frontmatter no provocan re-embed.
version del manifest cambia si y solo si algún contentHash cambia.
- Manifest atómico: nunca
500 parcial; 404 si slug inválido.
Versionado y caché
| Capa |
Mecanismo |
| Edge (Cloudflare) |
s-maxage=300 en cuerpos, s-maxage=60 en manifest. Invalidación por Last-Modified. |
| Bot |
Persiste manifest.version y contentHash por archivo en doc_hashes. Solo descarga .md cuyo hash cambió. |
| Resiliencia |
Bot cachea último manifest + cuerpos en disco. Si la landing falla, sirve la última versión conocida. |
Cambios concretos en este repo
Se añade:
src/content/config.ts — colección docs con schema (title, description, order).
src/content/docs/*.md — 7 archivos extraídos de los .astro actuales.
src/pages/docs/[...slug].astro — dynamic route único que renderiza la colección con Docs.astro.
src/pages/api/docs/manifest.json.ts — endpoint del índice.
src/pages/api/docs/[slug].md.ts — endpoint del cuerpo.
Se elimina:
src/pages/docs/{index,getting-started,api,models,examples,agents,apps}.astro.
Se refactoriza:
CodeBlock.astro y RateLimits.astro se invocan desde el pipeline de rendering (rehype / mapping de componentes), no inline por página.
Docs.astro recibe frontmatter de la entrada de colección en lugar de props sueltas.
Plan de migración por fases (sin downtime)
Fase 0 — Preparación
Extraer prosa de los 7 .astro a .md en src/content/docs/. Implementar [...slug].astro + endpoints. Verificar paridad visual con astro dev. PR coexiste con páginas viejas durante revisión.
Fase 1 — Cutover de la landing
Merge: las .md reemplazan a las .astro en el mismo deploy. Endpoints /api/docs/* empiezan a servir contenido real. El bot sigue con su corpus viejo — no hay desincronización nueva.
Fase 2 — Doble lectura del bot (modo sombra)
Bot con DocsClient habilitado, pero respuestas siguen viniendo de los .md locales. Loguea diferencias contra el corpus remoto. 24–48h de observación.
Fase 3 — Cutover del bot
Bot pasa a alimentarse exclusivamente del DocsClient. Tarea de refresh cada 15 min activa.
Fase 4 — Limpieza
Borrar bot/docs/knowledge/*. Documentar el flujo en ARCHITECTURE.md y README del bot.
Decisiones pendientes
- Markdown puro vs MDX en
src/content/docs/. Recomendado: Markdown puro (el endpoint sirve exactamente lo que hay en disco, el bot no necesita degradación).
- Refresh del bot. Recomendado: pull cada 15 min (sin webhooks ni secrets compartidos).
- Fallback offline en el bot. Recomendado: cache en disco; si la landing cae, el bot sigue respondiendo.
/api/docs/* público o autenticado. Recomendado: público (los docs ya lo son en HTML; habilita futuros consumidores).
- i18n. Single-locale (es) por ahora o el manifest incluye
locale desde ya.
Issue espejo
Crear issue equivalente en nan-discord-bot para la parte del cliente (Fases 2 y 3).
Contexto
La documentación está duplicada y desincronizada entre dos sitios:
nan/(este repo, landing) — fuente de verdad real, contenido correcto y actualizado.nan-discord-bot/— sirve documentación desdebot/docs/knowledge/, una copia obsoleta y parcial (3 archivos vs 7 páginas en la landing).Ejemplo de deriva ya en producción:
intro.md: "solicita tu API Key al Staff".index.astro: "genera tu API Key desde la sección API Keys de la plataforma".Objetivo: la landing como fuente única, con un contrato HTTP que el bot (y cualquier futuro cliente) consume sin parsear presentación.
Solución
Migrar el contenido de
src/pages/docs/*.astroa Astro Content Collections ensrc/content/docs/*.md. Exponer dos endpoints públicos en el mismo Worker:GET /api/docs/manifest.json— índice con slug, título, hash SHA-256 y URL de cada archivo.GET /api/docs/{slug}.md— Markdown crudo con frontmatter.El bot hace pull periódico al manifest, compara hashes con su SQLite (lógica ya existente) y solo re-embebe lo que cambió.
Flujo de datos
Contratos HTTP
GET /api/docs/manifest.json{ "version": "2026-05-23T18:22:11Z", "generatedAt": "2026-05-23T18:22:11Z", "entries": [ { "slug": "intro", "title": "Bienvenido a NaN", "description": "Visión general...", "order": 0, "contentHash": "sha256:9f2a...c4", "contentUrl": "/api/docs/intro.md", "lastModified": "2026-05-20T09:15:00Z" } ] }GET /api/docs/{slug}.mdReglas:
contentHashes del cuerpo (sin frontmatter): cambios cosméticos de frontmatter no provocan re-embed.versiondel manifest cambia si y solo si algúncontentHashcambia.500parcial;404si slug inválido.Versionado y caché
s-maxage=300en cuerpos,s-maxage=60en manifest. Invalidación porLast-Modified.manifest.versionycontentHashpor archivo endoc_hashes. Solo descarga.mdcuyo hash cambió.Cambios concretos en este repo
Se añade:
src/content/config.ts— coleccióndocscon schema (title,description,order).src/content/docs/*.md— 7 archivos extraídos de los.astroactuales.src/pages/docs/[...slug].astro— dynamic route único que renderiza la colección conDocs.astro.src/pages/api/docs/manifest.json.ts— endpoint del índice.src/pages/api/docs/[slug].md.ts— endpoint del cuerpo.Se elimina:
src/pages/docs/{index,getting-started,api,models,examples,agents,apps}.astro.Se refactoriza:
CodeBlock.astroyRateLimits.astrose invocan desde el pipeline de rendering (rehype / mapping de componentes), no inline por página.Docs.astrorecibe frontmatter de la entrada de colección en lugar de props sueltas.Plan de migración por fases (sin downtime)
Fase 0 — Preparación
Extraer prosa de los 7
.astroa.mdensrc/content/docs/. Implementar[...slug].astro+ endpoints. Verificar paridad visual conastro dev. PR coexiste con páginas viejas durante revisión.Fase 1 — Cutover de la landing
Merge: las
.mdreemplazan a las.astroen el mismo deploy. Endpoints/api/docs/*empiezan a servir contenido real. El bot sigue con su corpus viejo — no hay desincronización nueva.Fase 2 — Doble lectura del bot (modo sombra)
Bot con
DocsClienthabilitado, pero respuestas siguen viniendo de los.mdlocales. Loguea diferencias contra el corpus remoto. 24–48h de observación.Fase 3 — Cutover del bot
Bot pasa a alimentarse exclusivamente del
DocsClient. Tarea de refresh cada 15 min activa.Fase 4 — Limpieza
Borrar
bot/docs/knowledge/*. Documentar el flujo enARCHITECTURE.mdy README del bot.Decisiones pendientes
src/content/docs/. Recomendado: Markdown puro (el endpoint sirve exactamente lo que hay en disco, el bot no necesita degradación)./api/docs/*público o autenticado. Recomendado: público (los docs ya lo son en HTML; habilita futuros consumidores).localedesde ya.Issue espejo
Crear issue equivalente en
nan-discord-botpara la parte del cliente (Fases 2 y 3).