Hyperclay Local
  1. Hyperclay Local
  2. Docs
  3. Serving documents
  4. Data API

Data API

Read and write HTML and HTML Clay documents as structured JSON using CSS selector rules, with ETag and page revision protection.

Every document served by Hyperclay Local can be read and updated as structured JSON using CSS selector rules, either through a rules tag embedded in the document or through a data query parameter supplied by the caller. Every read and write carries an ETag (and, for page documents, page revision stamps) so stale writes are refused instead of overwriting newer edits.

For the browser plugin and hosted equivalents, see ClayJS data: pages as JSON, Hyperclay Page data endpoints, and HTML Clay JSON over HTTP.

Read JSON with an embedded rules tag#

Embed a JSON rules tag inside your .html or .htmlclay document:

HTML
<script type="application/json" data-rules-name="api" data-rules-version="1">
{ "title": "h1", "items": ".item[]" }
</script>

Then read the extracted JSON from /_/api/<file>:

Shell
curl http://localhost:4321/_/api/index.html
# {"title":"Notes","items":["a","b"]}

curl http://localhost:4321/_/api            # same as /_/api/index.html
curl http://localhost:4321/_/api/blog/post.html
  • data-rules-name: Parsed as a space-separated token list, so data-rules-name="api cms collection" matches the api token.
  • data-rules-version: Must be "1"; any other value returns 400 with {"error":"Unsupported rules version",...}.
  • Missing tag: Requesting /_/api/<file> without a data query parameter when the document has no matching api rules tag returns 400 with {"error":"No api rules tag",...}.

Read or write with caller-supplied rules (?data=)#

You can supply CSS selector rules directly on the request query string without adding a <script> tag to the document. Caller-supplied rules work on both the document’s own URL and /_/api/<file>, for both GET and POST, on .html and .htmlclay files:

Shell
# Read with relaxed-JSON rules on the document URL
curl 'http://localhost:4321/index.html?data={title:"h1",xs:".item[]"}'
# {"title":"Notes","xs":["a","b"]}

# URL-encoded GET on a .htmlclay document
curl --get 'http://localhost:4321/soup.htmlclay' \
  --data-urlencode 'data={title:h1}'

# POST with caller-supplied rules
curl --request POST \
  'http://localhost:4321/soup.htmlclay?data=%7Btitle%3Ah1%7D' \
  --header 'Content-Type: application/json' \
  --data '{"title":"Lunch"}'

Replace 4321 with your folder’s port if you are querying a team folder. The same ?data= query works on /_/api/soup.htmlclay?data=%7Btitle%3Ah1%7D.

  • On a document’s own URL, ?data= intercepts a GET or POST before the static file is served. Without ?data=, a GET to /index.html returns the raw HTML document.
  • Supplied rules replace the document’s embedded api mapping for that single request, even if the embedded tag is malformed. They are never merged with the embedded tag and never saved into the file.
  • An empty, malformed, or repeated data parameter returns 400 and never falls back to the embedded tag.

Caching#

  • Sidecar cache: Results extracted using an embedded api tag are cached in .hyperclay/api/ inside the served folder, rewritten on every save, and regenerated on a GET when the cache file is older than the source document. A freshly generated response includes the header X-Served-By: app-generated; a cache hit does not.
  • Per-filename isolation: Each complete source filename keeps its own JSON cache, so soup.html and soup.htmlclay answer independently.
  • Caller rules skip cache: Any request carrying a ?data= parameter bypasses the embedded mapping’s cache.
  • Legacy Tailwind URLs: Existing legacy Tailwind CSS URLs still share a basename: cold generation prefers .html, then falls back to .htmlclay if the .html file is missing.

Write JSON into a document (POST)#

Send a POST request with Content-Type: application/json to /_/api/<file> (or /_/api to target index.html, or /<file>?data=… with caller-supplied rules) to update the document’s DOM in place:

Shell
curl -X POST http://localhost:4321/_/api/index.html \
  -H 'Content-Type: application/json' \
  -d '{"title":"Groceries","items":["eggs","milk"]}'
# {"title":"Groceries","items":["eggs","milk"]}

POST bodies must be strict JSON with no rules envelope, using the same key structure that a GET with those rules returns. The response returns the fresh extraction under the same rules.

Content-only safety and validation rules#

  • Content only: Text values are written as text and never parsed as HTML. Attributes that could execute script or alter page behavior are refused: on* event handlers, style, srcdoc, save and region markers (no-save, no-data, contenteditable, and related attributes), and javascript:, vbscript:, and data: URLs. Nothing inside <script>, <style>, <template>, <iframe>, <object>, <noscript>, or <head> metadata tags can be written, and neither can @innerHTML or @outerHTML. A refused target returns 400 with nothing written:
    JSON
    {"error":"Write refused","details":[{"target":"<a#home>","reason":"..."}]}
    
  • Unknown keys and unmatched selectors: Keys not defined by the active rules, or rules whose CSS selector matches no element in the document, return 400 without writing anything:
    JSON
    {"error":"Write rejected","details":{"unknownKeys":[...],"unmatched":[...]}}
    
  • Partial writes and clearing values: Any key you omit from the JSON body is left untouched. Passing null clears a text value.
  • Lists: An array rule ([]) grows a list by cloning its first item, or by cloning its cms-template seed element when the list is currently empty.
  • Hidden regions: Elements marked no-data or editor-ui are invisible to both reads and writes.
  • Minimal byte changes and UTF-8 check: Only the targeted elements are rewritten; every other byte of the file stays as it was. Documents that are not valid UTF-8 are refused rather than corrupting bytes outside the edit. A POST body that changes nothing writes nothing to disk.
  • Body size and content type: Request bodies must carry Content-Type: application/json (415 otherwise) and be at most 1 MB (returning a JSON error when over the limit).
  • Origin rules: Browser requests must come from the same origin; command-line and script requests do not need a browser token.
  • Save pipeline and live updates: Every accepted write runs through the folder’s write gate the same way as a browser save: it writes a backup to .hyperclay/versions/, updates the file atomically, refreshes the .hyperclay/api/ cache, and broadcasts the change to open browser tabs as an external edit (protecting any region where someone is currently typing).

Refuse stale writes (If-Match and page revisions)#

Every GET and POST on the Data API reads the source file in a single pass and returns an ETag header that describes the exact bytes used for the extraction.

File ETag and If-Match#

Pass the ETag back in the If-Match header on your POST request to prevent overwriting concurrent edits:

Shell
curl -X POST http://localhost:4321/_/api/index.html \
  -H 'Content-Type: application/json' \
  -H 'If-Match: "9f86d081884c7d65"' \
  -d '{"title":"Groceries","items":["eggs","milk"]}'

If the document changed on disk since you read it, Hyperclay Local refuses the stale write with 412 Precondition Failed and leaves the file untouched.

Page documents and page revisions#

The Data API also reads and writes page documents whose editor merge state lives in .hyperclay/page-state:

  • A write to a page document names both the file revision and the page revision.
  • If the page revision is stale, the write is refused with the file on disk, the open live room, and .hyperclay/versions/ backups left unchanged.
  • When a Data API write based on an older file version can be merged into the stored page state, open windows merge the update instead of stopping.