The public website for the OpenPredicate specification — an open standard for JSON-encoded predicates.
The site's job is to make the standard discoverable and understandable: say what it is in one sentence, show it working before asking anyone to read anything, teach the two rules that surprise people, and then get out of the way of the normative text.
It also does one thing a README cannot. It serves the schema at its own $id, so
https://openpredicate.tech/schema/v0.4.0/open-predicate-schema.json resolves to the file it
identifies, and the $ref workflow the spec describes actually works. The RFC 9457 problem-type
URIs resolve too, one page each.
| Path | What it is |
|---|---|
/ |
The landing page: what it is, why, the two rules, and the integration points. |
/spec/ |
The specification, rendered from the vendored SPEC.md with a sticky contents sidebar. |
/operators/ |
Every operator, grouped by profile — generated from the schema at build time. |
/playground/ |
Write a filter, validate it against the real grammar in the browser. |
/problems/ |
The five error conditions, one dereferenceable page each. |
/schema/ |
Every published grammar version and its immutable URL. |
/changelog/ |
The changelog, rendered from the vendored CHANGELOG.md. |
npm install
npm start # build, then serve on http://localhost:4173
npm run build # render into dist/
npm test # build, then check the built siteNode 22 or newer. npm test asserts that every page and asset exists, that no internal link is
dead, that no template expression leaked into the output, that the operator reference covers every
operator in x-profiles, and that the playground's validator really accepts and rejects the right
filters.
Plain static HTML, rendered by build.mjs. No framework, no client-side routing, no
CSS pipeline — a specification site should be readable with JavaScript off, cacheable, and still
legible in ten years. The only script that ships is the playground's.
| File | Role |
|---|---|
build.mjs |
Renders every page into dist/. Fails the build rather than publishing something misleading. |
lib/layout.mjs |
The page shell, and the single source of the site's constants — version, URLs, contact. |
lib/markdown.mjs |
Renders the vendored spec documents, with GitHub-compatible heading anchors. |
lib/operators.mjs |
Builds the operator reference out of the schema. Throws if an operator in x-profiles has no definition. |
lib/validator.mjs |
Compiles the schema into a dependency-free browser validator, and self-checks it. |
lib/highlight.mjs |
Build-time syntax highlighting for JSON, YAML and HTTP. |
lib/home.mjs, lib/problems.mjs |
Page content. |
Most people meeting this project are not searching for "JSON-encoded predicates" — they are trying
to design a search endpoint. So /guide/ answers that question directly, in the
vocabulary people use, and resolves each decision by linking into the specification rather than
restating it.
The same content serves agents, which increasingly answer the question instead of a search results page:
/llms.txt— a short index, per the llmstxt.org convention, so an agent finds the right page in one fetch instead of guessing URLs./llms-full.txt—SPEC.mdverbatim plus the guide's questions and the changelog, so the whole normative text arrives in one request. Verbatim for the same reason/spec/renders the file rather than summarising it: a paraphrase of a specification is a second specification nobody maintains.schema.orgJSON-LD on every page, as one connected@graph—Organization,WebSite, and per page aTechArticleandBreadcrumbList— so the eight pages read as one work with one publisher. The guide adds aFAQPagegenerated from the same array that renders its prose, and a test asserts the two have not drifted.robots.txtnames the major AI crawlers and allows them explicitly.
Three deliberate constraints, because a specification site that contradicts its own specification is worse than no site:
- The spec page is the spec.
/spec/renders the vendoredSPEC.md— the normative file — not a summary of it. - The operator reference is generated. Descriptions, operand shapes and examples are read out of
open-predicate-schema.jsonat build time. Nothing is retyped. - The playground uses the real grammar.
lib/validator.mjscompiles the served schema into a standalone ES module with Ajv, bundles it, and runs a dozen known-good and known-bad filters through it before the build is allowed to finish. There is no third-party script and no runtime CDN.
The build also refuses to publish a schema whose $id is not the URL this site serves it at — a
schema published at any other address is a broken pin.
npm test # build, then check the built site
npm run test:layout # build, then measure it in a real browser
npm run test:all # bothtest.mjs checks the output: every page and asset exists, links resolve, the schema
sits at its own $id, the playground really rejects a malformed filter.
test-layout.mjs checks what only a browser can see — horizontal overflow,
computed contrast, target sizes, heading outline — across four viewport widths and both colour
schemes. Each of its assertions stands for a bug that reached production once: a bare 1fr grid
track letting a wide <pre> stretch the page past the viewport, a minmax(380px, …) that could not
fit a 342px container, a copy button left opacity: 0 until :hover on devices that never hover,
and four colour pairs under 4.5:1. All four were confirmed to fail the suite when reintroduced.
It drives Chrome over the DevTools Protocol through lib/browser.mjs — about
sixty lines, no driver dependency. Chrome's --window-size silently clamps to 500px, so the
viewport is set through Emulation instead; a screenshot taken at --window-size=390 is not a
390px render, and trusting one is how the overflow bug survived a visual check. Set CHROME_PATH to
pick a binary. With no browser present the suite fails rather than skipping.
The normative documents live in the specification
repository and are vendored here under
content/. Keeping up with a release has three parts, so that no step depends on someone
remembering it.
It is noticed. .github/workflows/sync-spec.yml checks the
upstream releases once a day. When the latest tag is not the one the site records, it syncs, builds,
tests, and opens a pull request with the diff. Merging is a human's call, and the workflow can be
run on demand from the Actions tab against any tag or branch.
It is one command. Nothing about the version is typed:
npm run sync # from main
npm run sync -- v0.6.0 # from a tag
npm test # then check the diffsync.mjs reads the grammar version out of the schema's own $id and writes the file to the path
that $id names, so a release that moves the grammar adds static/schema/vX.Y.Z/ instead of
overwriting a published one — those URLs are immutable, and /schema/ grows a row for the new
version on its own. The release version comes from the tag, or from the changelog when syncing an
untagged branch. Both land in content/spec-version.json, which is what
lib/layout.mjs reads; it is generated, so don't edit it by hand.
It cannot drift quietly. npm test fails if the version the site displays disagrees with the
vendored artefacts — including against CHANGELOG.md, which is fetched separately from the schema
and so is real evidence that both came from the same ref. A half-finished sync is a test failure
rather than a site that claims to document a release it never pulled in.
Syncing is a separate step from the build on purpose: a build never touches the network, so it is reproducible and cannot be broken by a push upstream.
ci.yml runs both suites on every pull request and every push to
main. The sync workflow also runs them itself, so that a sync whose tests fail still opens a pull
request with the result stated — that is the case a human most needs to see the diff for.
Netlify, configured by netlify.toml: npm run build, publish dist/. Pushes to
main deploy; pull requests get a preview, marked noindex so a preview cannot outrank the site.
The custom domain and its certificate are set in Netlify, not in the repository.
Two things the build hands the host:
dist/_headers, generated per published schema version. It sets the media type a$refconsumer expects (application/schema+json), CORS — the schema is fetched cross-origin by tooling and by this site's own playground — and a one-year immutable cache, which is only safe because a published$idnever changes.- Clean URLs and
404.html, which are Netlify defaults rather than configuration.serve.mjsimitates both, sonpm startshows what will actually be published.
MIT, the same as the specification. The logo and wordmark are in
static/assets/.