Skip to content

feat(docs): MDX content collection + public sync API#4

Open
Saul-Gomez-J wants to merge 6 commits into
mainfrom
feat/docs-content-collection
Open

feat(docs): MDX content collection + public sync API#4
Saul-Gomez-J wants to merge 6 commits into
mainfrom
feat/docs-content-collection

Conversation

@Saul-Gomez-J
Copy link
Copy Markdown

Summary

  • Migrar las páginas de docs (.astro por ruta) a una content collection MDX en src/content/docs/, con un único src/pages/docs/[...slug].astro que las renderiza y un Docs.astro que arma nav, breadcrumb y prev/next leyendo la propia colección.
  • Pulir la tipografía de .docs-content (h3, links, listas, blockquotes, tablas, imágenes), añadir toolbar + botón Copiar a cada <pre> de markdown y arreglar la regla de code inline (:not(pre) > code) para que los bloques Shiki no parezcan "seleccionados".
  • Exponer una API pública que el bot (y cualquier cliente) puede consumir como fuente única de verdad:
    • GET /api/docs/manifest.json — índice versionado con contentHash SHA-256 y contentUrl por 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ía lib/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 .astro y el bot servía un set de .md distinto bajo bot/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

  • Añadir un doc nuevo = crear src/content/docs/<slug>.md|.mdx con frontmatter (title, description, order). La nav y [...slug] se actualizan solos.
  • Los componentes en src/components/docs/ (ModelCard, LimitationsCard, EndpointGrid, FieldList, Callout) son lo único que necesita JSX, por eso models y api quedan en .mdx.
  • El htmlToText extrae 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.
  • En Limitations evita usar &lt; / &gt; 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.
  • Comprobar curl http://localhost:4321/api/docs/manifest.json | jqversion, entries[].contentHash, contentUrl.
  • Comprobar curl -i http://localhost:4321/api/docs/api.mdContent-Type: text/markdown, ETag, X-Content-Hash, sin HTML residual.
  • curl /api/docs/../etc/passwd.md y similares → 400.
  • curl /api/docs/no-existe.md → 404.
  • npm run build sin errores.

🤖 Generated with Claude Code

Saul-Gomez-J and others added 4 commits May 25, 2026 18:35
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>
Copy link
Copy Markdown
Contributor

@barckcode barckcode left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
  • contentHash frágil: cambio en ModelCard/EndpointGrid/Shiki/atributos data-* de Astro → cambia el HTML → cambia el hash → el bot re-embedde sin cambio semántico (cuesta tokens cada vez).
  • htmlToText.ts se 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 por order, 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-Hash coherentes.

Notas menores

  • Manifest.version se 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.ts no valida entry.id contra SAFE_SLUG; coherencia con [slug].md.ts.
  • Cache-Control max-age=60 vs docs_refresh_interval=900 en el bot → desfase que produce cache misses sin valor. Alinear.
  • El frontmatter que añade [slug].md.ts al body es ida-vuelta innecesaria (el bot ya tiene title/order/description desde 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 = false correcto para que la API sea siempre fresca.

@barckcode barckcode self-assigned this May 26, 2026
Saul-Gomez-J and others added 2 commits May 26, 2026 19:25
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>
@Saul-Gomez-J Saul-Gomez-J requested a review from barckcode May 26, 2026 17:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants