Skip to content

docs(api): serve the API reference with Scalar - #22

Merged
crstian19 merged 15 commits into
mainfrom
docs/scalar-api-reference
Aug 12, 2026
Merged

docs(api): serve the API reference with Scalar#22
crstian19 merged 15 commits into
mainfrom
docs/scalar-api-reference

Conversation

@crstian19

Copy link
Copy Markdown
Contributor

Closes #16.

/docs/api now renders from an OpenAPI spec with Scalar
instead of the 1,154 hand-written lines of api.mdx, which drifted from the
backend on every endpoint change. Updating an endpoint is now updating the spec.

Preview (serves no production traffic):
https://scalar-nan-website.helmcode.workers.dev

The spec

It starts from helmcode.com's and is rebranded, but not only the branding: the
nine resold models (claude-*, gpt-*, gemini-*), the prepaid-credit
billing and the 503 from their metering layer are gone, because NaN has none of
them. The 402 is rewritten as what it is here, the glm5.2 cap. mimo-v2.5,
glm5.2 and /mcp are added.

The surface was verified against api.nan.builders by probing every route: the
12 documented ones answer 401 (they exist and want auth) while moderations,
batches and files answer 404, so they stay out. Two inherited facts were
caught and corrected: the /v1/search limits (60 rpm / 3,000 a day / 10
concurrent, which are helmcode's; the real ones are 20 / 500 / 3) and an
enhance_prompt parameter NaN documents nowhere.

src/lib/openapiSpec.test.ts is the tripwire that keeps anything from
helmcode.com creeping back in.

Why the docs API still publishes api

The Discord bot builds its knowledge base from /api/docs/manifest.json. With
api.mdx out of the collection the manifest would have lost that entry, and
the bot does not fail on that: it silently drops the chunks. Serving the raw
spec is no good either, since it chunks by paragraph at 2,000 characters and a
JSON cut that way yields meaningless embeddings.

So openapiToText generates Markdown from the spec and /api/docs serves it
under the same slug, order and contract as before. Verified end to end with the
bot's own canonicaliser: all seven hashes match.

Documentation shell

The guides kept the previous look while /docs/api debuted the new one, so the
section had two looks one click apart. helmcode's DocsLayout is ported and
adapted to NaN's tokens: a top bar shared by guides and reference, a grouped
sidebar nav, search with Cmd+K, prev/next, TOC, footer and a skip link. The
#0a0a0a values and Tailwind's neutral-* scale are gone.

Two deliberate departures from the original: the nav groups live in the data (a
group field in the schema) rather than hand-written in the layout, and the
search indexes the content collection rather than import.meta.glob.

Worth a reviewer's attention

  • /openapi.json is no longer prerendered. The rate limits resolve at serve
    time from rateLimits.ts, which reads the env; prerendering would freeze them
    and /docs/api would contradict /docs/models.
  • preview_urls: true is now set on the production worker, permanently.
  • Deep links to the old anchors (/docs/api#chat-completions) will land on
    the page without scrolling: Scalar generates anchors in a different format.
    The one in-repo link, in agents.md, was updated.
  • The bot re-syncs on its next manifest poll.

Testing

642 tests, astro check and the production build all green. The shell tests
are verified by mutation: breaking each regression this branch fixed makes the
suite fail. Rendered pages were checked in a browser against the deployed
preview, measuring geometry rather than just asserting elements exist, after
that distinction cost a broken layout that presence checks did not catch.

/docs/api now renders from an OpenAPI spec instead of the 1,154 hand-written
lines of api.mdx, which drifted from the backend on every endpoint change.
Updating an endpoint is now updating the spec.

The spec starts from helmcode.com's and is rebranded to NaN, but not only the
branding: the nine resold models (claude-*, gpt-*, gemini-*), the prepaid
credit billing and the 503 from their metering layer are gone, because NaN has
none of them. The 402 is rewritten as what it actually is here, the glm5.2
cap. mimo-v2.5, glm5.2 and the /mcp endpoint are added, which helmcode does
not document.

The surface was verified against api.nan.builders by probing every route: the
12 documented ones answer 401 (they exist and want auth) while moderations,
batches and files answer 404, so they stay out. The /v1/search limits were
also corrected, as they carried helmcode's numbers (60 rpm, 3,000/day, 10
concurrent) instead of the real ones (20 rpm, 500/day, 3).

On the Discord bot: with api.mdx out of the collection the manifest would have
lost its `api` entry, and the bot does not fail on that, it silently drops the
chunks. We would have lost the content it gets asked about the most with no
alarm at all. Serving the raw spec was no good either: it chunks by paragraph
at 2,000 characters, and a JSON cut that way yields meaningless embeddings. So
openapiToText generates the Markdown from the spec and /api/docs serves it
under the same slug, order and contract as before. Verified end to end with the
bot's own canonicaliser: all seven hashes match and the reference splits into
28 clean chunks.

The theme is wired to src/styles/tokens.css rather than inheriting loose hex,
which helmcode left pending. "Ask AI" and "Generate MCP" are turned off by
configuration rather than by hiding the button: the component mounted anyway
and called api.scalar.com on every load. With withDefaultFonts: false the page
contacts no third party, which is the reason the rest of the site self-hosts
its fonts.

"Try it" goes straight to api.nan.builders (open CORS) without Scalar's proxy,
so a member's key never travels to a third party.

- /docs/api and /es/docs/api, without noindex and in the sitemap with hreflang
- the agents.md link to #mcp becomes #tag/mcp, Scalar's anchor
- a tripwire over the spec: surface, model catalogue, and nothing from helmcode
  creeping back in
Systematic comparison of the new spec against the api.mdx it replaces, which
was NaN's previous source of truth. Everything else lines up: the fields per
endpoint, the model enums (/completions qwen3.6, /responses qwen3.6+gemma4 and
so on), the reasoning mapping (reasoning_effort on deepseek-v4-flash,
chat_template_kwargs.enable_thinking on qwen3.6/gemma4) and the defaults.

enhance_prompt was the exception: /images/generations and /images/edits
declared it as a "NaN extension", wording that came out of mechanically
replacing "Helmcode extension", and api.mdx never mentions it, unlike seed and
guidance which are documented and stay. LiteLLM's spec does not know it either,
though it declares no body for that endpoint so that is not conclusive.

With no evidence it exists on NaN it goes: the issue's criterion is to document
nothing the backend does not serve. If it turns out to exist, adding it back is
one line.
Checked with a real key: gemma4 answers with finish_reason `tool_calls` and
emits get_weather({"city": "Madrid"}) given a prompt that asks for it. It also
accepts image input.

The model table came from api.mdx, which credited it with vision and reasoning
only. modelos.json did list it with tool calling: of the two sources that
contradicted each other, modelos.json was right on this point.

qwen3.6 stays as it was, which was correct: it emits tool calls and accepts
images, exactly as already published.
The page declared `--scalar-font: var(--font-sans)` (Archivo) but never
imported the fonts, so the browser had nowhere to get them from and fell back
to the system one. Zero .woff2 files requested.

It is the kind of fault code review does not catch: the CSS variables resolved
correctly and only the rendering was wrong. It surfaced by checking in the
browser which font the body actually used.

They are imported here the way Docs.astro and NanBase.astro do. Leaving them to
Scalar is not an option: it would serve them from fonts.scalar.com, which is
exactly what `withDefaultFonts: false` avoids.
Two cleanups with no functional effect.

Em dashes are removed from everything written on this branch: code, the
published spec and the messages. Replaced by colons, commas or parentheses as
each sentence called for, not by a bare hyphen. Pre-existing content that did
not come from here is left as it was.

And the comment language: the repo has two conventions and they had been mixed.
The /api/docs pipeline is in English (rateLimits.ts, mdxToText.ts and their
tests) and the layouts, styles and middleware in Spanish. The new files under
src/lib came out in Spanish, and there was even a Spanish block inside
canonicalParity.test.ts, which is entirely English. They move to English;
ApiReference.astro and scalar-theme.css stay Spanish, like their neighbours.
Both headers (/docs and /docs/api) rebuilt the brand by hand: the nan-logo.png
icon (a 600x600 PNG scaled to 26px) plus a <span>NaN</span> of text. They now
use the real wordmark, nan-wordmark-white.svg.

It goes in as a CSS MASK rather than an <img>, the way Nav.astro and
Footer.astro do on the site: the SVG supplies only the silhouette and
`background` supplies the colour, so it can turn violet on hover with a
transition. With an <img> the file would have to be kept in two colours.

nan-logo.png is left unused.
The values were right, but written by hand in openapi.json. That made them a
THIRD copy of numbers that already have a single source of truth in
src/lib/rateLimits.ts, the module that exists precisely because the page and
the API had drifted apart once (60 rpm against 100 rpm).

The risk was not theoretical: RATE_LIMIT_RPM and RATE_LIMIT_PARALLEL are
environment variables (wrangler.jsonc). Changing them updated /docs/models and
/api/docs/models.md while /docs/api kept publishing the old number.

The spec now carries a {{RATE_LIMITS}} placeholder resolved when it is served,
by rateLimitsToSpecMarkdown() alongside the other formatters. /openapi.json
stops being prerendered because of this: prerendering would freeze the numbers
at build time. It keeps a one-hour cache.

Five new tests, including one checking that an env override reaches /docs/api
and not only /docs/models.

Note: the web-search and image limits are still written in the spec, because
they are not in rateLimits.ts and no other surface publishes them today. If
they are added there, this same mechanism picks them up.
They are needed for `wrangler versions upload --preview-alias <name>`, which
uploads a version WITHOUT serving traffic and returns a stable URL at
<alias>-nan-website.<subdomain>.workers.dev. It is the only way we have of
showing a change before merging it: there is no dev environment, no domain and
nothing in nan-devops, and setting one up is separate infrastructure.

Cloudflare requires declaring it in the configuration; without this the command
still uploads the version but gives no URL.

The comment records that those URLs are public, unauthenticated and indexable
by default, because it matters here: this branch removes the noindex from
/docs/api and puts it in the sitemap.
The docs kept the previous look while /docs/api debuted Scalar's on the design
tokens, so the section had two looks one click apart and only one of the two
pages had a top bar. helmcode's DocsLayout is ported here, adapted to NaN's
system.

What lands:
- A SHARED top bar (DocsTopBar.astro) between the guides and the reference,
  which is what fixes the inconsistency. The EN/ES switcher is only rendered
  where a Spanish version exists, today just /docs/api: offering a language
  that leads to a 404 is worse than not offering it.
- A grouped sidebar nav (Get started / Reference / Guides). Unlike helmcode,
  which writes the groups by hand in the layout, they live in the data here: a
  `group` field in the collection schema and in API_DOC_META, with the order
  between groups taken from the lowest `order` in each. Adding a guide stays a
  matter of creating a file.
- Search with Cmd+K. helmcode indexes with import.meta.glob over .md pages;
  here the guides are a content collection, so the headings come from
  render(entry), and the reference's from the Markdown generated off the spec,
  which is its only textual representation.
- Prev/next with direction and title, TOC, footer and skip-to-content link.
- src/styles/docs-shell.css: helmcode's --doc-* layer fed by tokens.css. The
  #0a0a0a values and Tailwind's neutral-* scale are gone. Square corners and
  violet, as NaN's system asks for.

The TOC is still built from the DOM rather than from Astro's `headings`: the
h2s rendered by ModelCard do not appear in that list, so /docs/models would
have lost its whole index (10 entries).

Along the way, a pre-existing fault only visible in the console: Docs.astro's
CSP carried `font-src 'self'` and Vite turns the font subsets under the inline
limit into data URIs, so the browser blocked several. It now allows `data:`.
Exhaustive review of the old layout against the new one. The port took
helmcode's structure without checking what NaN's had on top.

CI WAS BROKEN, and it was mine: the CSP comment ended up in ATTRIBUTE position
inside the <meta>, which the TSX compiler behind `astro check` does not accept.
Six cascading errors that blew up the whole file to the end. I did not see them
because at commit time I only read the last lines of the output, which cut off
the error count. The comment becomes a sibling of the <meta>.

What had been lost:
- The breadcrumbs, entirely. helmcode does not have them and they fell out with
  the change of structure. Their styles had been unused ever since.
- The close button inside the mobile drawer. It did not get renamed: the
  affordance disappeared, and closing depended on the overlay, Escape or going
  back to the hamburger, none of them obvious on a small screen.
- `min-height`, `overflow-x` and font smoothing, which the previous layout
  carried as utilities on the <body>. Without the first the background does not
  cover the viewport on a short page; without the second a horizontal overflow
  stops being clipped.
- The sidebar nav's prefix matching: a nested route used to highlight its
  parent and became exact matching. With today's flat collection it does not
  show, which is what makes it dangerous.

CSS the port left orphaned, now removed: `.toc-link*` (the new JS no longer
sets that class; the index is painted through `.docs-toc a`), `.page-nav*`
(replaced by `.dpn`) and a `.docs-wm` I had duplicated across global.css and
docs-shell.css.

Verified in the browser: breadcrumbs on /docs/models, active nav, the close
button showing on mobile only and closing, and zero console errors.
…chors

The shell had no tests at all, and the port lost four things without anything
failing. These 36 tests cover exactly that, and writing them turned up a bug.

BUG FOUND: Scalar has TWO anchor namespaces and they are not interchangeable.
The overview sections are addressed as `description/<slug>` and the endpoint
groups as `tag/<slug>`. The search index labelled everything `tag/`, so
searching "Authentication", "Rate limits" or "Errors" produced a link to an id
that does not exist and the reader landed on the page without scrolling.
Checked against the ids Scalar paints on the deployed page.

The logic moves out of Docs.astro into src/lib/docsNav.ts so it can be tested:
a layout is only reachable through a full SSR render, and these are precisely
the parts that break silently.

What is guarded:
- The reference's anchors, a contract with @scalar/api-reference that nothing
  else checks. If a version changes the scheme, today the results point nowhere
  and no build fails.
- The nav's prefix matching, which today's flat collection makes invisible.
- That the breadcrumbs, skip link, drawer close button, TOC, prev/next, search
  and copy toast are all still there.
- That the <body> keeps min-height and overflow-x, and that the --doc-* layer
  still comes from the tokens.
- That no `order` repeats or leaves gaps, counting the reference's synthetic
  entry, and that every `group` is one of the known ones.

Verified by mutation: breaking each of the day's six regressions by hand makes
the suite fail on all six.
Checked against the API with an image of shapes and colours that cannot be
guessed: it describes the yellow square top left, the red circle centred and
the blue background. The control without an image invents that everything is
black, so there is no way to get that right by chance.

modelos.json was the one that was wrong, not the spec. The contradiction
predates this work: this file gave gemma4 tool calling and denied qwen3.6
vision, while the old api.mdx said the exact opposite of each. Both models have
both capabilities.

It is the model grid on the home page, so it was a false claim in production.
`.docs-inner` is a two-column grid: content and TOC. When the breadcrumbs were
restored I placed them as a sibling of the <main>, that is, a third child of
that grid. The result: the breadcrumbs took the first column, the content fell
into the TOC's 188px one and the index dropped to the next row. The page came
out one word per line.

Measured before fixing it: grid `786px 188px`, breadcrumbs at x=318 with 786 of
width and the content at x=1160 with 188. Now content at x=318 with 720 and the
index at x=1160 with 188, with no horizontal overflow at 1400, 1000 or 390.

The breadcrumbs move inside the <main>, where the previous layout had them.

The fault survived two of my own reviews because I checked that the elements
WERE THERE, not where they ended up: the DOM showed the breadcrumbs, the index
with its 10 entries and the right nav. The new test looks at placement rather
than presence, and it is verified by mutation that taking the breadcrumbs out
of the <main> makes it fail.
In the docs bar both links carrying an arrow leave the site, but the
nan.builders one was a relative path (`/`), so it kept you on whatever domain
you were browsing. On the Cloudflare preview that meant clicking "nan.builders"
took you to *.workers.dev.

It comes from porting helmcode's link, where `/` is correct because it is their
own domain. NaN's previous layout already had it absolute.

It becomes `https://nan.builders`, with the language prefix where it applies.
Finishes what an earlier commit started on src/lib. Every file this branch
creates or rewrites now carries English comments, and so do the blocks added
to files that already existed.

Pre-existing Spanish in files this branch did not author (sitemap.xml.ts,
middleware.ts, tokens.css and friends) is left alone: converting it is a
repo-wide job, not part of this migration.
@crstian19
crstian19 merged commit ae910da into main Aug 12, 2026
2 checks passed
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.

Docs: migrar la referencia de API a Scalar, como en helmcode.com

2 participants