Skip to content

Add self-hosted React docs app to replace Mintlify - #15

Merged
rjb32 merged 9 commits into
mainfrom
self-hosted-docs-app
Aug 17, 2026
Merged

Add self-hosted React docs app to replace Mintlify#15
rjb32 merged 9 commits into
mainfrom
self-hosted-docs-app

Conversation

@rjb32

@rjb32 rjb32 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Renders the existing .mdx content with the same design as the Mintlify-hosted site, as a static build that any host can serve. The .mdx files and docs.json remain the source of truth and are unchanged; docs.json still drives navigation.

Stack: Vite 6 + React 19, @mdx-js/rollup, Shiki (same library and themes Mintlify used), Tailwind v4 + typography, Mermaid, MiniSearch. npm run build emits dist/ with all 35 routes prerendered to real HTML plus a 404 fallback.

Design parity, measured against a captured snapshot of the live site:

  • 99.75% of 134,416 geometry assertions across 136 selectors x 35 pages
  • mean 1.10% per-slice pixel difference
  • 293/293 heading anchors reproduce the old site's ids exactly, so in-page and cross-page links keep resolving

Two non-obvious things the design depends on, both documented in theme.css: the prose overrides must be unlayered (the typography plugin emits into @layer utilities), and Mintlify applies a global body-weight rule that makes every font-semibold inert, so bold is signalled by colour rather than weight.

Deliberate departures from the old site:

  • No AI assistant UI (navbar button, per-code-block sparkle, floating ask bar) — all Mintlify SaaS with no self-hosted equivalent. Consequences: wider search field, 99px code padding instead of 131px, pages 86px shorter.
  • No "Powered by Mintlify".
  • Code set in IBM Plex Mono (OFL) rather than proprietary paperMono; the two measure an identical 8.0px advance at 14px, so code alignment is unchanged.
  • Footer social icons inlined as SVG instead of fetched from a FontAwesome CDN.
  • Images served locally, which lets style.css's image-rendering: pixelated rule finally apply to the pixel-art card icons.
  • Guides card grid has 16px spacing restored (style.css forced gap: 0).

Also included:

  • scripts/compare.mjs, the parity harness (geometry + pixel diff modes) that produced the numbers above
  • Dockerfile + nginx config + railway.json for deployment. Build context is the repo root, since the content is the app's input.
  • concepts/overview.mdx: point the Get Started link at /quickstart instead of the old turingdb.mintlify.app host

Renders the existing .mdx content with the same design as the Mintlify-hosted
site, as a static build that any host can serve. The .mdx files and docs.json
remain the source of truth and are unchanged; docs.json still drives navigation.

Stack: Vite 6 + React 19, @mdx-js/rollup, Shiki (same library and themes
Mintlify used), Tailwind v4 + typography, Mermaid, MiniSearch. `npm run build`
emits dist/ with all 35 routes prerendered to real HTML plus a 404 fallback.

Design parity, measured against a captured snapshot of the live site:
  - 99.75% of 134,416 geometry assertions across 136 selectors x 35 pages
  - mean 1.10% per-slice pixel difference
  - 293/293 heading anchors reproduce the old site's ids exactly, so in-page
    and cross-page links keep resolving

Two non-obvious things the design depends on, both documented in theme.css:
the prose overrides must be unlayered (the typography plugin emits into
@layer utilities), and Mintlify applies a global body-weight rule that makes
every font-semibold inert, so bold is signalled by colour rather than weight.

Deliberate departures from the old site:
  - No AI assistant UI (navbar button, per-code-block sparkle, floating ask
    bar) — all Mintlify SaaS with no self-hosted equivalent. Consequences:
    wider search field, 99px code padding instead of 131px, pages 86px shorter.
  - No "Powered by Mintlify".
  - Code set in IBM Plex Mono (OFL) rather than proprietary paperMono; the two
    measure an identical 8.0px advance at 14px, so code alignment is unchanged.
  - Footer social icons inlined as SVG instead of fetched from a FontAwesome CDN.
  - Images served locally, which lets style.css's image-rendering: pixelated
    rule finally apply to the pixel-art card icons.
  - Guides card grid has 16px spacing restored (style.css forced gap: 0).

Also included:
  - scripts/compare.mjs, the parity harness (geometry + pixel diff modes) that
    produced the numbers above
  - Dockerfile + nginx config + railway.json for deployment. Build context is
    the repo root, since the content is the app's input.
  - concepts/overview.mdx: point the Get Started link at /quickstart instead of
    the old turingdb.mintlify.app host
@mintlify

mintlify Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
turingdb 🟢 Ready View Preview Aug 3, 2026, 4:53 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

rjb32 added 2 commits August 3, 2026 19:05
The app was scaffolded under site/ so it could sit alongside the existing
Mintlify setup during the port. Mintlify is gone, so the split no longer earns
anything: content and app now share one root.

Layout changes:
  - site/{src,scripts,public,deploy,Dockerfile,package.json,...} -> repo root
  - fonts/, logo/, images/ and favicon.png -> public/
    public/ is now the single home for served assets. That removes the ~39 MB
    of duplication between images/ and site/public/images/ and lets
    scripts/copy-assets.mjs go entirely — there is nothing left to copy. The
    .mdx files reference these as absolute /images/... URLs, so no content
    changed.

Mintlify files removed:
  - README.md — was the unmodified "Mintlify Starter Kit" template; replaced
    with real documentation for this repo
  - style.css — Mintlify's custom-CSS injection point. All ten of its rules were
    ported into src/theme.css during the port, so it was dead weight.
  - snippets/snippet-intro.mdx — Mintlify snippet-convention boilerplate, never
    referenced by docs.json
  - docs.json: dropped "$schema" (pointing at mintlify.com) and "theme": "mint".
    Everything else is retained: name/colors/logo/favicon/navigation/navbar/
    footer are read by the app, and appearance/background/fonts are kept as
    design metadata.

Remaining mentions of Mintlify in the tree are comments explaining why a
measured value is what it is. Those are worth keeping.

Path references updated: vite.config.ts and gen-routes.mjs (contentRoot is now
the project root), src/docs.config.ts, theme.css @source globs, the Dockerfile
(context is the app root, so it is much simpler), railway.json, and the ignore
files.

Verified after the move: clean build produces 35 prerendered pages, tsc passes,
anchor parity still 293/293, Docker image builds and serves every route plus
assets with a correct 404. Geometry parity 99.74% — the delta from 99.75% is the
Guides card gap, now recorded in compare.mjs as intentional rather than left
looking like a regression.
475 KB image at the repository root with no reference from any page, config or
stylesheet. It also sat outside public/, so it was never served — it did not
appear in the build output, and removing it leaves dist/ byte-identical.

Not to be confused with public/images/zero_lock.png, a different image (208 KB,
different checksum) that concepts/zero_locking.mdx does use. That one stays.
@rjb32
rjb32 force-pushed the self-hosted-docs-app branch from 30ab828 to 5045c7a Compare August 3, 2026 17:27
rjb32 and others added 4 commits August 3, 2026 19:37
Adds .github/workflows/deploy.yml. There was no CI in this repo before — the old
setup used Mintlify's GitHub App, which builds nothing we control.

  pull request -> main : build check only (gen, tsc, anchor test, build, assert
                         all 35 pages prerendered)
  push        -> main : the same check, then `railway up`, then wait until the
                         live index.html byte-matches this commit's build and
                         smoke-test four routes plus a 404

Deploys are gated on `github.event_name != 'pull_request'`, so a fork PR can
never trigger one. Concurrency cancels in-flight PR checks but never an in-flight
deploy.

Requires a RAILWAY_TOKEN repository secret (a project token for the production
environment). It cannot be read in an `if:` expression, so the job checks it in
the shell and fails with an actionable message instead of half-deploying.

Two supporting changes, both needed to make the build job pass from a clean
checkout — found by replaying the job in a pristine copy of the tree:

  - `npm run gen` now runs before `tsc`. routes.generated.ts and
    search-index.generated.json are gitignored, so a fresh checkout has neither
    and the typecheck failed on 13 missing-module errors.
  - scripts/test-slug.mjs now checks against scripts/fixtures/anchors.json, a
    committed record of all 293 heading ids the previous site published, instead
    of reading a local capture directory that only exists on one machine. It
    crashed outright in CI before. `--update` regenerates the fixture from a
    capture. esbuild is now a declared devDependency rather than relied on as a
    transitive dep of Vite.
Content text moves from gray-400 (#A0A6A2, 7.9:1 against the page background) to
gray-200 (#E0E6E2, 15.5:1) — level with the headings.

Routed through a single token, --color-prose-text, rather than editing the nine
places that painted gray-400. Changing that one line now dials the whole site.
Sidebar, TOC and navbar keep gray-400 on purpose, so chrome still recedes behind
content.

<strong> had to change with it. The previous design signalled bold purely by
colour, because the global body-weight rule flattens every font-semibold to 300.
Against a gray-200 body, gray-50 bold separates by 1.20:1 and pure white by
1.27:1 — neither is perceptible. So <strong> now carries weight 500 and #fff, and
is exempted from the body-weight rule. Emphasis reads as emphasis again.

compare.mjs records both as intentional, but narrowly: instead of excluding
`color` on a growing list of selectors, it accepts two exact substitutions
(gray-400 -> gray-200, gray-50 -> white). Any other colour difference still
fails, so this cannot mask a real regression. Geometry parity stays at 99.75%.
…ressions

Typography
  Ark Pixel is no longer applied. It is a 16px bitmap face: fine at display
  sizes, but it was also carrying the TOC and the sidebar group titles at
  14-16px, where it is genuinely hard to scan. All text is now IBM Plex Sans.

  - added real weights: Regular 400, SemiBold 600, and a true Italic. Previously
    only Light 300 was loaded, so any bold or italic was synthesised.
  - body copy 300 -> 400. Light stems are thin at 16px on a dark background.
  - headings 400/+0.06em -> 600/-0.01em. The wide tracking existed to stop
    bitmap glyphs reading cramped; on a proportional face it looks loose.
  - TOC links and sidebar group titles 16px -> 14px with normal tracking. The
    1rem bump existed so the bitmap font landed on whole pixels.
  - <strong> and inline code follow the body to 600 and 400 respectively.

  The font file is still shipped: point --font-heading back at "Ark Pixel" in
  theme.css to restore the old look.

Preview URL resolution (real bug, found while checking the above)
  Every page except / threw React #418 — "server rendered text didn't match" —
  and silently discarded the prerendered tree. Cause was not the app: `vite
  preview` serves dist/index.html for any clean URL, so /quickstart was being
  hydrated against the home page's HTML. Production was always correct, because
  nginx does `try_files $uri $uri/index.html`.

  Added a preview-only middleware that resolves directory indexes the same way,
  so local preview matches production. All 14 sampled routes are now clean.

Two CSS regressions fixed
  - links inside a callout took the brand green instead of the callout's own
    colour. The generic .dark .mdx-content a.link rule ties on specificity and
    was declared later, so it won.
  - a <Steps> block lost its 40px top margin when it followed a heading, from
    the same margin-collapse rule the table wrapper was already exempt from.

compare.mjs
  Typography is now expressed as substitutions (weight 300->400, 400->600,
  300->600, 500->600; family ark pixel -> ibm plex sans) rather than blanket
  per-selector exclusions, so an unexpected weight or family still fails. Only
  the selectors whose font SIZE changed get a box/position exemption.
  99.66% of 126,156 assertions pass, 6 critical failures — down from 24 before
  the two fixes above.
Ark Pixel 16px carries the page title, every h1-h6 (step titles included), the
sidebar main group titles and "On this page". Everything that is scanned rather
than read once stays on IBM Plex Sans: body copy, sidebar page links, and the TOC
entries — which is the one place the previous revision was right to pull the
bitmap face, and the only spot where this differs from the original design.

Two things the face forces:

  - Weight 400, not 600. Ark Pixel ships one weight. Measured in Chromium, 500 is
    pixel-for-pixel identical to 400 and 600 synthesises a smeared bold (2671
    differing pixels on a 25-character string). So the 600 rules on headings and
    on the nav labels are gone, and headings return to 400 with +0.06em tracking —
    the tracking is what stops bitmap glyphs reading cramped. A link inside a
    heading now inherits the heading's weight instead of asking for 600.
  - The two nav labels are pinned to 1rem, up from 0.875rem. 16px is not a size
    choice but the font's design size; at 14px every stem falls on 7/8ths of a
    device pixel and the glyphs grey out. The original did the same, for the same
    reason.

New --font-nav-label token for the sidebar group titles; --font-nav keeps the
proportional stack for the nav items below them. "On this page" is a real <h2> and
takes --font-heading. Both stacks match the reference's exactly.

compare.mjs
  Display type matches the captured reference again, so most of the typography
  exemptions no longer earn their keep and are removed — family, size, weight and
  tracking on headings, the page title and the nav labels are now checked rather
  than skipped. What remains is the body weight (Light -> Regular, the one change
  that reflows prose), the TOC entries, and the 16px "On this page".
  99.67% of 127,945 assertions pass, 6 critical failures — the same six the last
  commit documented as pre-existing, on ~1,800 more assertions. Mean per-slice
  pixel difference 3.76%, down from 4.20%.

Sidebar auto-scroll (outside the typography change)
  16px group titles put the sidebar back in the regime REST_RATIO was fitted for,
  which exposed a 3-4px miss on four graph_dev pages that the blanket exemption
  had been hiding. Re-solved against all 35 reference offsets: 0.44 -> 0.437, which
  is 35/35 within 2px (was 31/35, worst 4px). Preferred over re-adding an exemption
  that would blind the harness to real sidebar regressions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
rjb32 and others added 2 commits August 17, 2026 16:01
Light is a display weight, so it is applied where the type is large enough to
carry thin stems and nowhere else:

  page title  36px  300      h3  20px  400
  h1          30px  300      h4  18px  400
  h2          24px  300      h5/h6  16px  400

Below 24px the size step over 16px body copy is small, and a 300-weight heading
ends up physically thinner than the paragraph it introduces — hierarchy running
backwards, across 95 h3s in the content. The cut-off is asserted twice, in the
prose block and in the #content-area rule, because the `!important` that beats
`font-semibold` would otherwise flatten the h3-and-below rule.

Tracking goes with the face: +0.06em -> -0.01em. The wide setting existed to give
bitmap glyphs air; on a proportional face it reads loose.

Ark Pixel keeps the two nav labels — the sidebar group titles and "On this page" —
at its own 16px, weight 400/500, +0.08em. "On this page" now reads
--font-nav-label rather than --font-heading: it is an <h2> in the DOM but a nav
label on the page, and the old coupling meant it followed the titles anywhere they
went. That is what lets the title face change on its own.

Nothing new is shipped: the Light cut of IBM Plex Sans was already loaded — it
carried the whole site's body copy before 05c2d00.

compare.mjs
  The titles leave the reference again (family, weight and tracking all move), so
  their letterSpacing/width/height/rect exemptions come back, plus a 400->300
  weight substitution for the display sizes and a fontFamily exemption on the TOC
  header, whose stack now differs from the reference's in its fallbacks only.
  99.65% of 127,521 assertions pass, 6 critical failures — the same six,
  pre-existing and none typographic. Mean per-slice pixel difference 3.94%,
  between Ark Pixel's 3.76% and SemiBold's 4.20%.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The sidebar group titles and "On this page" follow the titles onto Plex Light, so
Ark Pixel is now applied nowhere. Two settings went with the face, both of which
existed only to serve a 16px bitmap grid:

  - the +0.08em tracking, which was spacing out bitmap glyphs. A proportional
    label wants 0.
  - the inherited weights. The sidebar label sat at 400 and the TOC header at 500
    (its font-medium class, which the global body-weight rule leaves alone because
    the button is an `h2 *`). Both are asserted at 300 now, or the TOC header would
    out-weigh the sidebar label beside it.

The 1rem stays. It was a pixel-grid accommodation rather than a design choice, so
dropping it to 0.875rem would have been defensible — but at 300 the label is
already lighter than its own 14.4px/400 items, and size plus colour (gray-200 over
gray-400) are what still make it read as a heading. At 14px it would be smaller,
lighter and barely brighter than the list it heads.

Ark Pixel is still shipped in public/fonts and either --font-heading or
--font-nav-label brings it back in one line; theme.css, the README and
THIRD-PARTY-NOTICES all say so rather than leaving a dead 58KB asset unexplained.
The two tokens are kept separate for the same reason they earned their keep here:
the seam is what let the titles and the labels move one at a time.

compare.mjs
  The labels leave the reference on tracking and weight, so: a 500->300
  substitution for the TOC header, letterSpacing on it as well (ours is now
  `normal`, which the numeric ≤1px tolerance cannot bridge), and letterSpacing plus
  width on the sidebar labels. Their font SIZE is unchanged, so nothing below them
  moves and their rects stay checked.
  99.62% of 127,354 assertions pass, 6 critical failures — the same six,
  pre-existing and none typographic. Mean per-slice pixel difference 4.09%; the
  trend as the titles moved is 3.76% (Ark Pixel), 3.94% (Plex Light titles), 4.09%
  (labels following).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@rjb32
rjb32 merged commit 714293b into main Aug 17, 2026
2 of 3 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.

1 participant