feat(docs): MDX content collection + public sync API#4
Conversation
Replace per-page .astro files with an Astro content collection (src/content/docs/) so docs are a single source of truth that drives both the rendered website and a public, machine-readable API consumed by the Discord bot (and future clients). Site changes: - Add @astrojs/mdx integration. - Define a docs collection with frontmatter schema (title, description, order, locale) loaded via glob. - New dynamic route src/pages/docs/[...slug].astro renders entries; the layout reads the collection for nav/breadcrumb so adding a doc no longer requires editing route or nav lists. - Extract reusable rich UI primitives (ModelCard, LimitationsCard, EndpointGrid, FieldList, Callout) so MDX stays readable. - Polish .docs-content typography (h3/links/lists/blockquotes/tables/ images), wrap markdown <pre> with a labeled toolbar + copy button, and constrain the inline-code background rule with :not(pre) > code so Shiki blocks no longer pick up the "selected" look. Public API: - GET /api/docs/manifest.json — versioned index with per-entry sha256 content hash and contentUrl. - GET /api/docs/[slug].md — server-renders the live page, extracts the article between <!--ARTICLE-START--> / <!--ARTICLE-END--> markers, and converts the HTML to clean Markdown via lib/htmlToText.ts. - Responses set Cache-Control, ETag and X-Content-Hash. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…lection # Conflicts: # src/content/docs/examples.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous commit incorrectly renamed "### curl" section headings to "### bash" along with code fence languages. The headings denote the request method (curl vs SDK) rather than a syntax highlight language, so they must stay as "curl". Code fences remain as "bash". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
barckcode
left a comment
There was a problem hiding this comment.
Gracias Saúl, dirección 100% correcta (web como source of truth + manifest con sha256 + diff selectivo en el bot). Antes de mergear y promover DOCS_USE_REMOTE=remote, tres blockers y un par de notas menores.
Blockers
1. [slug].md.ts no debe re-renderizar la página y deshacer el HTML
Hoy el endpoint hace fetch(${origin}/docs/${slug}) → SSR completo (layout, sidebar, Shiki, MDX components) → recorta entre marcadores → lo pasa por htmlToText.ts (140 LOC de regex). Y manifest.json.ts lo hace N veces en paralelo por cada hit.
Problemas concretos:
- N sub-requests SSR por hit al manifest.
contentHashfrágil: cambio enModelCard/EndpointGrid/Shiki/atributosdata-*de Astro → cambia el HTML → cambia el hash → el bot re-embedde sin cambio semántico (cuesta tokens cada vez).htmlToText.tsse romperá silenciosamente con cualquier cambio del runtime Astro/MDX.
Cambio propuesto: servir entry.body (el body crudo del MDX en la content collection) y procesarlo con remark + remark-mdx para convertir los componentes JSX (ModelCard, EndpointGrid, FieldList, Callout, LimitationsCard) a markdown plano vía visitor sobre los nodos mdxJsxFlowElement / mdxJsxTextElement. Cero SSR, cero parser HTML; depende solo de nuestros componentes (estable).
Esto elimina htmlToText.ts y los marcadores <!--ARTICLE-START/END--> del layout.
2. ETag se emite pero no se honra If-None-Match
Ambos endpoints generan el ETag pero no leen request.headers.get('if-none-match') → nunca devuelven 304. Patch trivial; es lo que justifica generar el hash. También mejora el shared cache en Cloudflare.
3. Falta toda la batería de tests
No se añade ningún test en una PR que introduce un parser de HTML, dos endpoints públicos y un hash que será consumido por otro servicio. Mínimo para mergear:
lib/contentHash.ts: vacío, ASCII, UTF-8 multibyte.- Serializer JSX→MD (lo que sustituye a
htmlToText): fixture por cada componente rich + un caso compuesto. - Endpoint
manifest.json: shape del payload, ordenación pororder, ETag estable para el mismo input,If-None-Match→ 304. - Endpoint
[slug].md: slug inválido → 400, slug inexistente → 404, body sin HTML residual, ETag +X-Content-Hashcoherentes.
Notas menores
Manifest.versionse calcula pero el bot no lo usa. O lo aprovechas como short-circuit ("si la version no cambió, no toques nada") o lo eliminas.manifest.json.tsno validaentry.idcontraSAFE_SLUG; coherencia con[slug].md.ts.Cache-Control max-age=60vsdocs_refresh_interval=900en el bot → desfase que produce cache misses sin valor. Alinear.- El frontmatter que añade
[slug].md.tsal body es ida-vuelta innecesaria (el bot ya tienetitle/order/descriptiondesde el manifest y lo descarta con_strip_frontmatter). Sirve solo el body.
Lo que está bien y conviene preservar
- Migración a content collection MDX con
[...slug].astroúnico — añadir doc = añadir fichero, nav/breadcrumb/prev-next derivados de la colección. - Componentes ricos en
src/components/docs/extraídos limpios. - Pulido de
.docs-content(toolbar + copy, fix de:not(pre) > code). prerender = falsecorrecto para que la API sea siempre fresca.
Address PR #4 review blockers and shape the contract the bot will sync against. - Replace the SSR+htmlToText pipeline with a remark-based mdxToText that works on entry.body. Strips mdxjsEsm imports/exports, converts our MDX components (ModelCard, LimitationsCard, EndpointGrid, FieldList, Callout, RateLimits) and author-written HTML (h1-h4, a, code, strong/b, em/i, br, span) to markdown, then normalises to a stable canonical text. Unknown JSX components throw to fail closed. - Rewrite /api/docs/manifest.json and /api/docs/[slug].md to hash the canonical text. Both endpoints honour If-None-Match (304 with Cache- Control + ETag + X-Content-Hash), validate slug with the shared SAFE_SLUG, and serve Cache-Control: public, max-age=900, s-maxage=900 aligned with the bot's docs_refresh_interval. - Manifest version is sha256 of [(slug, contentHash)] sorted, giving the bot a stable short-circuit. - Drop the ARTICLE-START/END markers from Docs.astro and delete src/lib/htmlToText.ts; they are no longer reachable. - Add nodejs_compat to wrangler.jsonc so the unified/remark stack runs on the Cloudflare Workers dev runtime. - Tests: contentHash (empty/ASCII/UTF-8), docsApi helpers (SAFE_SLUG, Cache-Control, If-None-Match parsing), mdxToText against fixtures per component plus corpus invariants (no imports/exports, no residual HTML, every used component mapped, unknown component throws), and endpoint integration tests for both routes (shape, ordering, stable version/ETag, 304, per-entry hash matches body endpoint). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- mdxToText: replace TS2352 casts with index-signature access - tsconfig: exclude tests so astro check no longer needs node:* types Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Summary
.astropor ruta) a una content collection MDX ensrc/content/docs/, con un únicosrc/pages/docs/[...slug].astroque las renderiza y unDocs.astroque arma nav, breadcrumb y prev/next leyendo la propia colección..docs-content(h3, links, listas, blockquotes, tablas, imágenes), añadir toolbar + botón Copiar a cada<pre>de markdown y arreglar la regla decodeinline (:not(pre) > code) para que los bloques Shiki no parezcan "seleccionados".GET /api/docs/manifest.json— índice versionado concontentHashSHA-256 ycontentUrlpor entrada.GET /api/docs/{slug}.md— re-renderiza la página, extrae el artículo entre<!--ARTICLE-START-->/<!--ARTICLE-END-->y devuelve Markdown limpio víalib/htmlToText.ts(sin clases, sin estilos, conservando indentación de bloques de código y entidades decodificadas una vez al final). Headers:Cache-Control,ETag,X-Content-Hash.Por qué
Hasta ahora cada doc vivía en su propio
.astroy el bot servía un set de.mddistinto bajobot/docs/knowledge/, lo que obligaba a editar dos repos para cada cambio y dejaba al bot fuera de sync. Con la colección única, la web es la fuente de verdad y la API entrega el mismo contenido en texto plano para indexar (sin ruido HTML).Notas para revisar
src/content/docs/<slug>.md|.mdxcon frontmatter (title,description,order). La nav y[...slug]se actualizan solos.src/components/docs/(ModelCard,LimitationsCard,EndpointGrid,FieldList,Callout) son lo único que necesita JSX, por esomodelsyapiquedan en.mdx.htmlToTextextrae los bloques de código a placeholders (CODE_BLOCK_N) antes de colapsar whitespace y los restaura al final — así no se pierde indentación en los fenced blocks.Limitationsevita usar</>dentro de strings JS de props: son literales JS, no HTML; ya quedan literales</>.Test plan
npm run dev— visitar/docs,/docs/api,/docs/models, etc. y verificar nav, breadcrumb, prev/next y toolbar de Copiar.curl http://localhost:4321/api/docs/manifest.json | jq—version,entries[].contentHash,contentUrl.curl -i http://localhost:4321/api/docs/api.md—Content-Type: text/markdown,ETag,X-Content-Hash, sin HTML residual.curl /api/docs/../etc/passwd.mdy similares → 400.curl /api/docs/no-existe.md→ 404.npm run buildsin errores.🤖 Generated with Claude Code