Skip to Content
shapekit

The website

site/ is the website for shapekit.org: the documentation under /docs and a dashboard over the committed results under /dashboard. The dashboard shows which shape families and CAPs the corpus covers, which SDK version diverges on which entry, what the wallet-backend reference run found, and the findings list. The site root redirects to /docs; a landing page can replace that one route module (site/src/routes/home.tsx) without touching the docs or the dashboard.

Every page is prerendered to a static HTML file at build time, so the site needs no server-side code: any static file server can host it. It has no analytics, no cookies and no third-party requests. Fonts (IBM Plex Sans and IBM Plex Mono) are bundled, docs search runs in the browser over an index file the build writes, and the only thing kept in the browser is the colour theme choice, in localStorage under shapekit-theme. The production server sends a content security policy that only allows the site's own origin, so a third-party request would fail.

Stack

React Router 8.5.1 in framework mode on Vite 8.3.8, with ssr: false and a prerender list (site/react-router.config.ts); the documentation uses Fumadocs (fumadocs-core and fumadocs-ui 16.19.0, fumadocs-mdx 15.6.1); styles are Tailwind CSS 4 with shadcn/ui components. Fumadocs takes its colours from the same shadcn variables as the dashboard (fumadocs-ui/css/shadcn.css), so the docs and the dashboard share one palette in the light and the dark theme, and one navigation bar. Versions in site/package.json are exact and site/package-lock.json is committed. ESLint is not used because typescript-eslint does not support TypeScript 7; oxlint covers the React, hooks and accessibility rules instead.

Routes

RouteWhat it isPrerendered as
/redirect to /docs (a meta refresh, works without JavaScript)index.html
/docs, /docs/<group>/<page>one page per document, see belowdocs/.../index.html
/api/search.jsonthe docs search index, downloaded by the search dialogapi/search.json
/dashboardoverviewdashboard/index.html
/dashboard/coverage, /dashboard/coverage/<CAP>coverage per CAP, one page per CAP in coverage.jsondashboard/coverage/.../index.html
/dashboard/sdk, /dashboard/indexer, /dashboard/findingsSDK matrix, indexer run, findingsdashboard/<page>/index.html
any other addressnot-found page__spa-fallback.html, served with status 404

Docs pages are complete HTML and read without JavaScript. Dashboard pages are prerendered as their navigation with a loading state; the browser then fetches /data/*.json and renders the page. Filters live in the query string (/dashboard/sdk?entry=...), so a filtered view can be linked. Old dashboard links of the form /#/sdk are redirected to /dashboard/sdk. Next to each route's HTML file the build writes a .data file that the client router fetches on in-site navigation.

Docs come from docs/

docs/ and README.md stay the only copies of the documentation. Before every build site/scripts/docs-content.ts writes site/content/docs/ (ignored by git) from them:

GroupPageSource
Getting started/docs (Introduction)README.md: introduction, "What is in it", "Infrastructure and privacy", "Maintenance", "License"
/docs/installREADME.md: "Install" and "Building and checking"
/docs/quick-startREADME.md: "Quick start"
Corpus/docs/corpus/adding-a-shapedocs/adding-a-shape.md (the sidebar also links the dashboard's coverage page)
Harness/docs/harness/adapter-interface, /docs/harness/replay-serverdocs/adapter-interface.md, docs/replay-server.md
SDK conformance/docs/sdk/testee-protocol, /docs/sdk/sdk-cidocs/testee-protocol.md, docs/sdk-ci.md
Linter/docs/linter/indexerlintdocs/linter.md
Guide/docs/guide/indexer-hardeningdocs/guide/indexer-hardening.mdx
Operations/docs/operations/infrastructure, maintenance, protocol-upgrade-runbook, differential-fuzzing, sitethe docs/ file of the same name

On the way the script:

  • takes each page's title from its first heading and its description (used for the page's meta description and for search) from its first sentence; the readme-derived pages have fixed descriptions, and the guide keeps the title and description of its own frontmatter;
  • rewrites relative links between these files to site routes, including anchors, and links to any other repository file to https://github.com/ErenTopaal/shapekit/blob/main/... (tree/ for a directory); a link to a file that does not exist is reported on the console;
  • adapts the guide, which is written for stellar-docs: {#id} heading ids become Fumadocs' [#id], <Warning> becomes a warn callout, its site-absolute links point at developers.stellar.org, and a callout at the top says it is a draft pending SDF review whose canonical copy will be on developers.stellar.org.

Each docs page ends with a link to the repository file it was generated from. To add a page, add the document under docs/ and list it in the groups table of the script.

Where the numbers come from

Every number the dashboard shows is read from JSON that shapekit site data writes. The command (crates/shapekit-cli/src/site_cmd.rs) reads committed files only, never a run output under target/, so CI rebuilds the same data from a checkout:

File readWhat it gives the site
corpus/manifest.jsonevery entry: family, CAPs, protocol, source class, expected decode, file paths
corpus/coverage/coverage.jsonper-CAP, per-family, per-fragment and per-arm coverage, and the summary
registry/*.tomlCAP texts (linked at the pinned stellar-protocol commit), XDR additions, family priority and protocol range
findings/triage.tomlthe paths of the committed harness report, conformance matrices and linter result
findings/inputs/*those reduced run outputs, as findings/triage.toml names them
findings/findings.jsonthe findings record and its summary
conformance/versions.tomlthe SDK version matrix and the current version of each SDK
conformance/known-failures/*.txtknown-failure lines per SDK and version
conformance/reproductions.tomlthe upstream sightings the runner must reproduce
pins.tomlpinned upstream versions shown on the overview

The commit is passed with --commit; the command does not read git. It writes six files: meta.json (commit, repository, corpus header, pins, and the path, size and sha256 of every file it read and of every file it wrote), coverage.json, entries.json, conformance.json, harness.json and findings.json. The output is deterministic: the same inputs and flags give the same bytes.

Two things the site shows are derived rather than copied, and both are plain counts over the committed files:

  • SDK totals per version: failing cells by result, cells with a known issue, and failing checks by operation, counted over the committed matrix. The committed matrices keep failing cells only (see the reduction field of findings/inputs/provenance.json), so an entry that passed or was unsupported at every version is not listed.
  • Reproduction rows: for each version a row names, whether the committed matrix has every named check of the row failing (before the fix) or no counted check failing (after it). Rows built from probes are not in the matrix and say so. shapekit conformance reproduce remains the check that actually runs them.

crates/shapekit-cli/tests/site_data.rs runs the command over the committed files and checks that the summaries equal the source files', that per-version and per-family counts add up to the source totals, that every linked file exists, that the recorded sha256 values match, and that two runs give identical bytes.

Building it locally

Needs the Rust toolchain of the workspace and Node.js 26.13.1.

cargo build --release --locked -p shapekit-cli
target/release/shapekit site data --out site/public/data --commit "$(git rev-parse HEAD)"
cd site
npm ci
npm run typecheck       # generates the docs content and route types, then tsc
npm run lint            # oxlint
npm run dev -- --host   # development server
npm run build           # prerenders every route into dist/client
npm run preview         # serves dist/client with the production caddy configuration (docker), port 5182

site/public/data/ and site/content/ are ignored by git; both are always regenerated. Run npm run preview again after a build, since the build replaces dist/.

Checks in CI

.github/workflows/site.yml runs on pull requests and pushes to main that touch the site, its docs or its inputs. It builds the CLI, runs shapekit site data, then npm ci, the typecheck, the linter and the build; checks that every docs page, every CAP page and every dashboard page has a prerendered file; validates both caddy configurations with the pinned caddy image; and uploads site/dist/client as an artifact. It does not deploy: the site is hosted on the owner's server.

Hosting

The site runs on azure-m (20.106.204.221), a server shared with other projects. One caddy container, edge-caddy in /opt/edge, owns ports 80 and 443, gets every TLS certificate, and forwards each host name to a project container over the external docker network edge; /opt/edge/Caddyfile imports one file per project from /opt/edge/sites/. The files in deploy/azure-m/ follow that layout:

FileInstalled asWhat it does
docker-compose.yml/opt/shapekit/docker-compose.ymlthe shapekit-site container: caddy pinned by digest (the image the edge runs), /opt/shapekit mounted read-only at /srv, on the edge network, restart: unless-stopped, no published ports
Caddyfile/opt/shapekit/Caddyfileserves /srv/current: try_files {path} {path}/index.html, Cache-Control: public, max-age=31536000, immutable for /assets/* (hashed names) and no-cache for everything else, the content security policy, and __spa-fallback.html with status 404 for unknown addresses
shapekit.caddy/opt/edge/sites/shapekit.caddyshapekit.org with encode zstd gzip, HSTS and no Server header, proxied to shapekit-site:80; www.shapekit.org redirects permanently to the apex
deploy.shbuilds and ships a release, see below
preview.shruns the same image and Caddyfile locally on port 5182 (npm run preview)

The server is reachable only through the owner's Mac, so deploy.sh runs every remote command as ssh mac "ssh azure-m <command>"; the user name and address come from the SSH config on the Mac. SHAPEKIT_SSH_JUMP and SHAPEKIT_SSH_HOST change the two host aliases.

DNS

Create these records at the registrar or DNS provider of shapekit.org before the first deploy, so the edge can get the certificates:

TypeNameValue
Ashapekit.org20.106.204.221
Awww.shapekit.org20.106.204.221

No AAAA records: the server has no global IPv6 address (checked with ip -6 addr show scope global, which lists none). The records must point at the server directly, not through a proxying CDN: the edge obtains its certificates with caddy's default ACME challenges, which reach the server itself.

Deploying

From a clean checkout of the commit to publish:

deploy/azure-m/deploy.sh --dry-run   # build and validate, read the server, change nothing
deploy/azure-m/deploy.sh

The script:

  1. builds the CLI, writes the site data for HEAD, runs npm ci, the typecheck, the linter and the build, and checks that the main pages exist in site/dist/client;
  2. validates Caddyfile and shapekit.caddy locally with the pinned caddy image;
  3. packs site/dist/client into a tarball and reads the server's state (edge container, edge network, current release, disk space);
  4. creates /opt/shapekit if it is missing (with sudo, owned by the deploying user), streams the tarball into /opt/shapekit/site-<commit> (unpacked next to it first, then renamed);
  5. installs docker-compose.yml and Caddyfile, points /opt/shapekit/current at the new release with an atomic rename, runs docker compose up -d, restarts the container only if its Caddyfile changed, and checks that shapekit-site answers from inside edge-caddy;
  6. installs sites/shapekit.caddy, runs caddy validate inside edge-caddy on the whole edge configuration, and only then caddy reload; if validation fails it puts the previous file back and stops, so the other sites on the edge are never affected;
  7. deletes all but the five newest releases, never the current one.

It refuses to run on a working tree with uncommitted changes, because the site data is labelled with the commit; --allow-dirty overrides that. --dry-run does steps 1 to 3 and prints the commands of the remaining steps instead of running them.

To roll back, point current at an older release on the server; the container serves it on the next request:

ssh mac "ssh azure-m 'cd /opt/shapekit && ls -dt site-*/ && ln -sfn site-<commit> current.new && mv -Tf current.new current'"

This page is generated from docs/site.md in the repository.

On this page