You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
api: audit against the ReSed API-design criteria — 10 findings, 2 broken published URLs #111
The talk Introduction to APIs by Peter Dängeli and Elena Spadini set out design criteria for the API of a digital scholarly edition. The slides are at https://pad.dsl.unibe.ch/resed-api-20260129. This issue records an audit of the live site against those criteria.
TextRefs meets most of them. Two published claims are false, because the URL they name returns 404. Three design choices are correct but unstated, so a reader cannot tell a decision from an omission.
Every finding below comes from a live request on 31 August 2026, first with curl and then in Chrome. The deployment served the staging branch at cd9aae3. The browser pass matters, because three behaviours depend on JavaScript or on redirect following.
What the audit confirms as strong
Resource design. Four record types: work, system, ref, and mapping. The talk asks for 3 to 5 core resources.
Granularity. One passage under one citation system. The talk names this the "meaningful scholarly unit", and warns against both a whole-file dump and a per-glyph endpoint.
Representations. Each record has an HTML page and a .json sibling. The head advertises <link rel="alternate" type="application/ld+json">. Verified on /id/work/homer.iliad/.
REST behaviour.GET and HEAD return 200. POST /reg/works.json returns 405. The site is stateless.
Caching. The host sends etag, last-modified, and cache-control: max-age=600. A request with If-None-Match returns 304.
Browser clients.access-control-allow-origin: * on every JSON response.
Access control. No authentication, which the talk endorses for public-domain content.
Linked Data without SPARQL. JSON-LD with a context at /contexts/v1.jsonld. The talk recommends this trade-off for a small team.
Bulk access./dump/ serves four NDJSON files, one alias table, and a Frictionless datapackage.json with a byte count and a sha256: hash per resource. The talk does not ask for this.
Honest documentation.api/openapi.yaml:32-39 records that a .jsonl body arrives as application/octet-stream, and tells clients to parse by shape.
A working API explorer./find/ resolved the query Plato Republic 514a to the work, two Perseus editions, a citation to copy, and a draft warning. It writes ?q= into the URL, so a result is linkable. It queries the same public endpoints any other client uses. The talk says an explorer "lowers the barrier to entry dramatically", and TextRefs has one.
Short aliases resolve in a browser./cite/homer.iliad/1.1 landed on the canonical record page. Verified in Chrome.
Status
Five of the ten findings are fixed and merged into staging (2026-08-31). The other five stay
open here, and each needs a decision before anyone writes code or prose.
The five fixes are on staging only. They reach the live site when staging deploys, and they
reach main with the release in #4. Re-run the curl block below after that deploy: the four
404s for G1, G4, G5 and G8 must become 200.
Findings
Ordered by severity.
G1 — The OpenAPI file is not published (high) — fixed
astro.config.mjs:55 advertises "OpenAPI at /api/openapi.yaml" in the site description. GET /api/openapi.yaml returns 404.
starlight-openapi reads api/openapi.yaml at build time and renders HTML at /api/. Nothing copies the source file into the output. find dist -name "*openapi*" returns no result, and no script in package.json or scripts/ copies it.
A client can therefore read the docs, but cannot generate code, run contract tests, or import the API into a tool such as Hoppscotch. The talk names OpenAPI as the industry standard, and values it because tools consume the file.
The site makes a claim that the site does not meet.
G2 — /cite/ and /find/ are absent from the contract (high)
api/openapi.yaml has no path item for /cite/{work_key}/{locator} or /cite/{work_key}/{system_key}/{locator}. AGENTS.md:28 describes both. /find/ is also undocumented, although it is the browser tool that the talk calls an "API explorer".
The live behaviour also differs from the word "redirect" in the site description. GET /cite/homer.iliad/1.1 returns 301 to the trailing-slash form. That URL returns 200 with an HTML meta refresh, not a 303.
A browser follows the meta refresh correctly. Chrome landed on /id/ref/cf079227-2d9a-5734-aa05-efd6777d7220/. A client that follows HTTP redirects only stops at the meta-refresh page and never reaches the record. The defect therefore affects scripts, crawlers, and reference managers, not people.
The talk asks for a "complete list with descriptions". The undocumented surface is the one a scholar pastes into a footnote.
G3 — The declared 404 body does not match the served body (medium) — fixed
Every record path in api/openapi.yaml declares 404 as application/json against the Error schema at api/openapi.yaml:594-599. GET /id/work/does-not-exist.json returns 404 with content-type: text/html and the Starlight 404 page.
A static host cannot produce a per-path JSON error. Declare what the host sends, and either remove the Error schema or mark it reserved.
G4 — No robots.txt and no stated limit (medium) — fixed
GET /robots.txt returns 404. The talk's access-control slide states that one misconfigured crawler can overwhelm a small project, and that a rate limit now protects availability.
TextRefs inherits the GitHub Pages soft limits. /dump/aliases.json is 2.5 MB and /dump/references.jsonl is 1.7 MB. Nothing tells a crawler to prefer /dump/ over the thousands of record pages, and nothing tells a heavy consumer what the limits are.
G5 — The published JSON Schema does not exist (medium) — fixed
src/content/docs/standard/specification.md:402 states that a normative JSON Schema "is published at https://textrefs.org/schemas/v1/textrefs.schema.json". That URL returns 404, and so does /schemas/. ROADMAP.md lists the same path under "Next" as planned.
The specification states as fact what the roadmap states as planned. zod version 4 is already a dependency and can emit JSON Schema, so either publish the file or correct the tense.
G10 — The production 404 page carries a draft banner (medium)
Every 404 page shows a yellow aside that reads "This content is a draft and will not be included in production builds." The banner is live on https://textrefs.org, so the statement contradicts itself.
The cause is src/content/docs/404.mdx:6, which sets draft: true. Starlight keeps the 404 route in the production build through a direct getEntry() lookup, and renders the draft notice with it.
Correction. An earlier version of this issue called this a one-line fix. That is wrong. The draft: true flag is load-bearing. Commit 666dc78 added it on purpose, to stop Starlight rendering /404 twice: once through its dedicated route and once through the [...slug] catch-all, which produced a build warning. Removing the flag brings the warning back and re-adds /de/404/.
The fix must therefore keep the flag and suppress the banner for this route, through a component override. Alternatively, move the custom 404 out of the docs collection.
The talk's error-handling slide states that an error message is part of the user experience of an API. This banner tells a visitor that a working production page is unpublished.
This finding needed the browser. A curl check of the status code alone does not show it.
G6 — No stated versioning policy for the HTTP surface (low, rising)
versioning.md versions the standard, the registry data, and the data package. The HTTP paths carry no version. Only the JSON-LD context does, at /contexts/v1.jsonld.
The talk lists three strategies: URL versioning, header versioning, and "only add, never break". TextRefs appears to follow the third, and ADR-0004 supports it. The talk's point is that an unstated policy is the sustainability risk, because a consumer cannot plan against it.
Severity rises at the first active record.
G7 — No query parameters, and the choice is unstated (low)
The talk calls pagination "essential for large result sets", and lists limit, page, sort, format, and fields. No TextRefs endpoint supports any of them, because a static host cannot process a query string.
One exception exists. /find/?q= is a real query parameter, but the browser reads it, not the host. It is undocumented.
This is the correct trade-off. The talk's own conclusion is that a simple and maintained API beats a complex and abandoned one, and a static site survives the departure of its developer. TextRefs already covers the need three ways: /reg/work/{key}/refs/{page}/ paginates as HTML, the JSON collections are small enough to fetch whole, and /dump/ serves the bulk consumer.
Two payloads are large. /reg/work/new-testament/aliases.json is 406 KB and /dump/aliases.json is 2.5 MB. Both are cacheable and both support 304.
The gap is not the missing parameters. The gap is that /api/ never states the choice. A reader who arrives from the talk expects limit and page, finds nothing, and cannot tell a decision from unfinished work.
/reg/ returns a browsable index. GET /dump/ returns 404. A reader who removes the filename from a dump URL finds nothing, and datapackage.json is an entry point that a reader cannot guess.
G9 — The German alternate on record pages is broken (low)
Record pages carry <link rel="alternate" hreflang="de" href="https://textrefs.org/de/id/work/homer.iliad/">. That URL returns 404. AGENTS.md:44-46 limits the German mirror to Association pages, so no German record page exists.
This finding is outside the talk's scope. It was found during the same probe. The effect is on search engines only.
The largest open question
All 23 works have status: draft. Under ADR-0004 a draft identifier carries no persistence promise. The talk asks whether a design survives two years, five years, and the departure of its first developer. The API shape survives that test. The promotion of records from draft to active is the step that turns a well-shaped API into a citable one, and no finding above changes that.
G1 and G5 were the two false published claims, so they came first. All five landed in three PRs
on 2026-08-31, in the order #117 → #119 → #118.
Two review findings on those PRs are worth recording here, because both were substantive:
The generated JSON Schema first emitted format: "uri" for z.url(). RFC 3986 forbids
non-ASCII, and 1219 references carry a resolver target such as …/Investigaciones_filosóficas_(edición_A)#76. The document declares format: "iri"
instead, and all 86477 records validate.
The same document dropped the §6 rules on alternative_labels. uniqueItems now travels
from standard/schema/work.ts into the published document, and the half that compares two
members — no entry repeats preferred_label — is stated in the field description, because
no keyword expresses it.
The other findings stay here for now. G2, G6, and G7 need a decision before anyone writes prose. G9 and G10 are both Starlight behaviour, and neither is a one-line change.
G10 is split out and ships in v0.1.0. PR #126 targets staging.
The cause was not a component override. src/content/docs/404.mdx carried draft: true, and Starlight renders DraftContentNotice from that flag alone. Removing the flag also required one edit to the sitemap filter, because the localized /de/404/ alias then entered the sitemap. The status table above can be updated when #126 merges.
This issue keeps the post-v0.1.0 label for the four findings that remain open: G2 (/cite/ and /find/ absent from the contract), G6 (no stated HTTP versioning policy), G7 (no query parameters, choice unstated), and G9 (German alternate on record pages is broken).
G10 was pulled into the release on the same ground as #3 and #99: the tag freezes a false published claim, and the claim is live on https://textrefs.org today.
Summary
The talk Introduction to APIs by Peter Dängeli and Elena Spadini set out design criteria for the API of a digital scholarly edition. The slides are at https://pad.dsl.unibe.ch/resed-api-20260129. This issue records an audit of the live site against those criteria.
TextRefs meets most of them. Two published claims are false, because the URL they name returns 404. Three design choices are correct but unstated, so a reader cannot tell a decision from an omission.
Every finding below comes from a live request on 31 August 2026, first with
curland then in Chrome. The deployment served thestagingbranch atcd9aae3. The browser pass matters, because three behaviours depend on JavaScript or on redirect following.What the audit confirms as strong
work,system,ref, andmapping. The talk asks for 3 to 5 core resources..jsonsibling. The head advertises<link rel="alternate" type="application/ld+json">. Verified on/id/work/homer.iliad/.GETandHEADreturn 200.POST /reg/works.jsonreturns 405. The site is stateless.etag,last-modified, andcache-control: max-age=600. A request withIf-None-Matchreturns 304.access-control-allow-origin: *on every JSON response./contexts/v1.jsonld. The talk recommends this trade-off for a small team./dump/serves four NDJSON files, one alias table, and a Frictionlessdatapackage.jsonwith a byte count and asha256:hash per resource. The talk does not ask for this.api/openapi.yaml:32-39records that a.jsonlbody arrives asapplication/octet-stream, and tells clients to parse by shape./find/resolved the queryPlato Republic 514ato the work, two Perseus editions, a citation to copy, and a draft warning. It writes?q=into the URL, so a result is linkable. It queries the same public endpoints any other client uses. The talk says an explorer "lowers the barrier to entry dramatically", and TextRefs has one./cite/homer.iliad/1.1landed on the canonical record page. Verified in Chrome.Status
Five of the ten findings are fixed and merged into
staging(2026-08-31). The other five stayopen here, and each needs a decision before anyone writes code or prose.
5914e26(#117, closes #112)/cite/and/find/absent from the contract5914e26(#117, closes #113)robots.txt, no stated limitcf3a416(#119, closes #114)0607ec6(#118, closes #115)/api//dump/has no index pagecf3a416(#119, closes #116)The five fixes are on
stagingonly. They reach the live site whenstagingdeploys, and theyreach
mainwith the release in #4. Re-run thecurlblock below after that deploy: the four404s for G1, G4, G5 and G8 must become 200.
Findings
Ordered by severity.
G1 — The OpenAPI file is not published (high) — fixed
Fixed in
5914e26(#117), which closes #112.astro.config.mjs:55advertises "OpenAPI at /api/openapi.yaml" in the site description.GET /api/openapi.yamlreturns 404.starlight-openapireadsapi/openapi.yamlat build time and renders HTML at/api/. Nothing copies the source file into the output.find dist -name "*openapi*"returns no result, and no script inpackage.jsonorscripts/copies it.A client can therefore read the docs, but cannot generate code, run contract tests, or import the API into a tool such as Hoppscotch. The talk names OpenAPI as the industry standard, and values it because tools consume the file.
The site makes a claim that the site does not meet.
G2 —
/cite/and/find/are absent from the contract (high)api/openapi.yamlhas no path item for/cite/{work_key}/{locator}or/cite/{work_key}/{system_key}/{locator}.AGENTS.md:28describes both./find/is also undocumented, although it is the browser tool that the talk calls an "API explorer".The live behaviour also differs from the word "redirect" in the site description.
GET /cite/homer.iliad/1.1returns 301 to the trailing-slash form. That URL returns 200 with an HTML meta refresh, not a 303.A browser follows the meta refresh correctly. Chrome landed on
/id/ref/cf079227-2d9a-5734-aa05-efd6777d7220/. A client that follows HTTP redirects only stops at the meta-refresh page and never reaches the record. The defect therefore affects scripts, crawlers, and reference managers, not people.The talk asks for a "complete list with descriptions". The undocumented surface is the one a scholar pastes into a footnote.
G3 — The declared 404 body does not match the served body (medium) — fixed
Fixed in
5914e26(#117), which closes #113.Every record path in
api/openapi.yamldeclares404asapplication/jsonagainst theErrorschema atapi/openapi.yaml:594-599.GET /id/work/does-not-exist.jsonreturns 404 withcontent-type: text/htmland the Starlight 404 page.A static host cannot produce a per-path JSON error. Declare what the host sends, and either remove the
Errorschema or mark it reserved.G4 — No
robots.txtand no stated limit (medium) — fixedFixed in
cf3a416(#119), which closes #114.GET /robots.txtreturns 404. The talk's access-control slide states that one misconfigured crawler can overwhelm a small project, and that a rate limit now protects availability.TextRefs inherits the GitHub Pages soft limits.
/dump/aliases.jsonis 2.5 MB and/dump/references.jsonlis 1.7 MB. Nothing tells a crawler to prefer/dump/over the thousands of record pages, and nothing tells a heavy consumer what the limits are.G5 — The published JSON Schema does not exist (medium) — fixed
Fixed in
0607ec6(#118), which closes #115.src/content/docs/standard/specification.md:402states that a normative JSON Schema "is published athttps://textrefs.org/schemas/v1/textrefs.schema.json". That URL returns 404, and so does/schemas/.ROADMAP.mdlists the same path under "Next" asplanned.The specification states as fact what the roadmap states as planned.
zodversion 4 is already a dependency and can emit JSON Schema, so either publish the file or correct the tense.G10 — The production 404 page carries a draft banner (medium)
Every 404 page shows a yellow aside that reads "This content is a draft and will not be included in production builds." The banner is live on https://textrefs.org, so the statement contradicts itself.
The cause is
src/content/docs/404.mdx:6, which setsdraft: true. Starlight keeps the 404 route in the production build through a directgetEntry()lookup, and renders the draft notice with it.Correction. An earlier version of this issue called this a one-line fix. That is wrong. The
draft: trueflag is load-bearing. Commit666dc78added it on purpose, to stop Starlight rendering/404twice: once through its dedicated route and once through the[...slug]catch-all, which produced a build warning. Removing the flag brings the warning back and re-adds/de/404/.The fix must therefore keep the flag and suppress the banner for this route, through a component override. Alternatively, move the custom 404 out of the docs collection.
The talk's error-handling slide states that an error message is part of the user experience of an API. This banner tells a visitor that a working production page is unpublished.
This finding needed the browser. A
curlcheck of the status code alone does not show it.G6 — No stated versioning policy for the HTTP surface (low, rising)
versioning.mdversions the standard, the registry data, and the data package. The HTTP paths carry no version. Only the JSON-LD context does, at/contexts/v1.jsonld.The talk lists three strategies: URL versioning, header versioning, and "only add, never break". TextRefs appears to follow the third, and ADR-0004 supports it. The talk's point is that an unstated policy is the sustainability risk, because a consumer cannot plan against it.
Severity rises at the first
activerecord.G7 — No query parameters, and the choice is unstated (low)
The talk calls pagination "essential for large result sets", and lists
limit,page,sort,format, andfields. No TextRefs endpoint supports any of them, because a static host cannot process a query string.One exception exists.
/find/?q=is a real query parameter, but the browser reads it, not the host. It is undocumented.This is the correct trade-off. The talk's own conclusion is that a simple and maintained API beats a complex and abandoned one, and a static site survives the departure of its developer. TextRefs already covers the need three ways:
/reg/work/{key}/refs/{page}/paginates as HTML, the JSON collections are small enough to fetch whole, and/dump/serves the bulk consumer.Two payloads are large.
/reg/work/new-testament/aliases.jsonis 406 KB and/dump/aliases.jsonis 2.5 MB. Both are cacheable and both support 304.The gap is not the missing parameters. The gap is that
/api/never states the choice. A reader who arrives from the talk expectslimitandpage, finds nothing, and cannot tell a decision from unfinished work.G8 —
/dump/has no index page (low) — fixedFixed in
cf3a416(#119), which closes #116./reg/returns a browsable index.GET /dump/returns 404. A reader who removes the filename from a dump URL finds nothing, anddatapackage.jsonis an entry point that a reader cannot guess.G9 — The German alternate on record pages is broken (low)
Record pages carry
<link rel="alternate" hreflang="de" href="https://textrefs.org/de/id/work/homer.iliad/">. That URL returns 404.AGENTS.md:44-46limits the German mirror to Association pages, so no German record page exists.This finding is outside the talk's scope. It was found during the same probe. The effect is on search engines only.
The largest open question
All 23 works have
status: draft. Under ADR-0004 a draft identifier carries no persistence promise. The talk asks whether a design survives two years, five years, and the departure of its first developer. The API shape survives that test. The promotion of records fromdrafttoactiveis the step that turns a well-shaped API into a citable one, and no finding above changes that.How to reproduce
With
curl:In a browser, because
curlcannot show these three:https://textrefs.org/cite/homer.iliad/1.1. The page lands on the record. G2 affects non-browser clients only.https://textrefs.org/find/and typePlato Republic 514a. The result appears, and the URL gains?q=. This is the undocumented explorer in G7.https://textrefs.org/robots.txt. Read the yellow aside on the 404 page. This is G10.Suggested split
This issue is the tracker. Five findings have sub-issues:
5914e26(fix(api): publish the contract and correct the 404 declaration (#113, #112) #117)5914e26(fix(api): publish the contract and correct the 404 declaration (#113, #112) #117)robots.txt—cf3a416(feat(site): add the /dump/ index and robots.txt (#116, #114) #119)0607ec6(feat(standard): publish the generated JSON Schema at /schemas/v1/ (#115) #118)/dump/index page —cf3a416(feat(site): add the /dump/ index and robots.txt (#116, #114) #119)G1 and G5 were the two false published claims, so they came first. All five landed in three PRs
on 2026-08-31, in the order #117 → #119 → #118.
Two review findings on those PRs are worth recording here, because both were substantive:
format: "uri"forz.url(). RFC 3986 forbidsnon-ASCII, and 1219 references carry a resolver target such as
…/Investigaciones_filosóficas_(edición_A)#76. The document declaresformat: "iri"instead, and all 86477 records validate.
alternative_labels.uniqueItemsnow travelsfrom
standard/schema/work.tsinto the published document, and the half that compares twomembers — no entry repeats
preferred_label— is stated in the field description, becauseno keyword expresses it.
The other findings stay here for now. G2, G6, and G7 need a decision before anyone writes prose. G9 and G10 are both Starlight behaviour, and neither is a one-line change.