- Hyperclay Local
- Docs
- Serving documents
- 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:
<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>:
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, sodata-rules-name="api cms collection"matches theapitoken.data-rules-version: Must be"1"; any other value returns400with{"error":"Unsupported rules version",...}.- Missing tag: Requesting
/_/api/<file>without adataquery parameter when the document has no matchingapirules tag returns400with{"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:
# 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 aGETorPOSTbefore the static file is served. Without?data=, aGETto/index.htmlreturns the raw HTML document. - Supplied rules replace the document’s embedded
apimapping 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
dataparameter returns400and never falls back to the embedded tag.
Caching#
- Sidecar cache: Results extracted using an embedded
apitag are cached in.hyperclay/api/inside the served folder, rewritten on every save, and regenerated on aGETwhen the cache file is older than the source document. A freshly generated response includes the headerX-Served-By: app-generated; a cache hit does not. - Per-filename isolation: Each complete source filename keeps its own JSON cache, so
soup.htmlandsoup.htmlclayanswer 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.htmlclayif the.htmlfile 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:
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), andjavascript:,vbscript:, anddata:URLs. Nothing inside<script>,<style>,<template>,<iframe>,<object>,<noscript>, or<head>metadata tags can be written, and neither can@innerHTMLor@outerHTML. A refused target returns400with nothing written:{"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
400without writing anything:{"error":"Write rejected","details":{"unknownKeys":[...],"unmatched":[...]}} - Partial writes and clearing values: Any key you omit from the JSON body is left untouched. Passing
nullclears a text value. - Lists: An array rule (
[]) grows a list by cloning its first item, or by cloning itscms-templateseed element when the list is currently empty. - Hidden regions: Elements marked
no-dataoreditor-uiare 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
POSTbody that changes nothing writes nothing to disk. - Body size and content type: Request bodies must carry
Content-Type: application/json(415otherwise) and be at most1MB (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:
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.