Repository navigation
Releases: cloudhal-dev/cloudhal
Release list
Cloudhal 0.6.2
All @cloudhal/* packages and create-cloudhal at 0.6.2.
Changes
-
createMiniflareDoSqlite() sends its Durable Object calls over kept-alive connections instead of opening a new TCP connection per call through Miniflare 4's dispatchFetch, so long suites no longer exhaust local ports on macOS (#116).
-
Migration
0029_entry_list_values(RFC-0042 §4, #110):entry_list_values, the indexed list-field values of entries (field, presence, number, text, collection-sort key, creation time, node, locale and status) with a query index and a per-node index, andentry_list_state, each listed collection's readiness (definition fingerprint, ready flag, backfill epoch and cursor). DDL only; existing data is untouched. -
The
./testingsubpath now ships. The published packages pointed./testingatdist/src/testing.jsand.d.ts, but the build left those files out, so importing@cloudhal/{storage,analytics,comments,schedules}/testingfailed with ERR_MODULE_NOT_FOUND. The suites run on your own vitest, 3 or later, which is an optional peer dependency.@cloudhal/storage's Miniflare helpers also load your ownminiflare, an optional peer of any version. The ranges are wide on purpose. When a direct dependency's optional peer does not match the copy in your project, npm stops with ERESOLVE, even if nothing imports./testing. No semver range matches the prerelease miniflare that wrangler currently brings in, so with a narrower range npm would install a second miniflare beside it. -
importSite(behindcloudhal pull, the admin bundle routecloudhal pushuses, andcloudhal import) now checks a bundle's project row before inserting it: it must be the site node's project (projectId), a valid Project, with itsidandrevcolumns matching its body. Before, a bundle could plant any projects row. -
Dependency patch updates (#115).
@cloudhal/apidepends on Better Auth 1.7.12 and on exactly the versions it uses itself:@better-auth/coreand@better-auth/api-key1.7.12,@better-auth/utils0.5.0 andbetter-call1.4.4.@cloudhal/cloudflareasks for zod ^4.6.7 (GHSA-mrhx-c7xc-m3w5 is fixed in 4.6.6).@cloudhal/core-tsand@cloudhal/astro-rendereruse marked ^18.1.1.@cloudhal/core-tsand@cloudhal/schedulesuse temporal-polyfill 1.0.6.@cloudhal/astro-rendereruses vite ^8.3.5. The Studio shipped in@cloudhal/cliis built with React 19.3.1, TanStack Router 1.170.46 and TanStack Query 5.104.5. marked 18.1.1 splits a few unusual markdown inputs differently: a blockquote ends before an unquoted line that follows a heading, rule or HTML block, and indented HTML blocks interrupt a paragraph. A stored body like that keeps rendering and shows virtual block ids until its next write. -
RFC-0042 §4, #110:
queryContentEntries(the API'sGET /entries?where=…, Studio's lists, MCPlist_entries) answerswhere,orderBy, status, locale,limitandoffsetin SQL from the newentry_list_valuesside table once a collection is ready, instead of reading and evaluating the whole collection; results are the shared evaluator's, which stays the fallback (term filters, collections not ready, operands SQL cannot compare exactly) and the oracle of a seeded property suite on SQLite, D1 and Durable Object SQLite. On the 10,000-entry benchmark the store path drops from 124 ms CPU / 10,001 rows (SQLite) and 176 ms / 10,003 rows read (D1) to 0.6 ms / 22 rows and 5.3 ms / 5,068 rows read; a page at offset 4,980 costs about the same as the first.The store keeps the table exact. Entry creates, updates, moves and deletes write an entry's rows (its
listFieldsvalues asresolveFieldsreads them) in their own batch, and an original's write rewrites its same-site translations' rows; site writes and field operations that change a collection's list definitions reset its readiness in the same batch, and imports reset the site's. A race never refuses a write: it marks the collection not ready. NewrebuildListValues(sql, options)is the resumable, budgeted backfill (guarded per chunk, safe to run concurrently;budget.statementsbounds every statement it issues,startrotates where a run begins,tidyremoves rows of unlisted collections);updateSite, finished field operations andimportSiterun it for what they reset, andopenLocalStoreruns it on open. A field operation's revert resets every listed collection whose values its restored bodies change.deleteContentEntrybehaves as before.revertTagsToTermsnow leaves an entry that was edited after the migration's before-snapshot (by a concurrent write, or before a resumed run) as it is and reports it inedited, instead of restoring its earlier fields, status or collection. -
importSite, used bycloudhal pull, by the admin bundle routecloudhal pushuses, and bycloudhal import, now runs every check before it writes anything, the bootstrap conflict and the 1000-statement limit included, and plans the media ledger with them. A refused bundle leaves the target as it was; before, node bodies could land ahead of a conflict and snapshots ahead of the size limit. Only a storage failure can stop it between its writes, and importing the bundle again completes it. New checks, each before the first write:- Every node body the target lacks must be a valid contract document of the kind of each snapshot ref that points to it, with that ref's id and rev, of the bundle's site (precompiled validators). A body no snapshot references is refused. Before, a body was only checked against its hash, so a crafted bundle could put invalid content behind the published snapshot.
- Node and snapshot ids may not belong to another site of the target, each appears once, and no two nodes share a kind and key or a translation group and locale (the node batch's unique keys).
- Every takedown entry has its fields with their types (
reason,legal,by,at). - The media ledger (
snapshot_blobs) reads the target's own asset bodies, never the bundle's text for a body the target already holds.
New
planImport(to, bundle, options)returns what an import would change, without writing:- the nodes added, changed and removed;
- the snapshots and node bodies to add;
- whether the project row is new;
- the listed media that are present, missing, of another size or taken down on the target;
- the bundle's takedowns, those whose bytes the target holds (an import deletes them) and those its ledger does not hold yet.
With
report: trueit returns a bootstrap conflict and missing media instead of throwing them. A node body that is not JSON is refused asblob <hash> is not JSONinstead of with the parser's message. -
New
restoreBundle(from, snapshot): a site bundle of one snapshot's nodes, rebuilt from the stored node bodies, forimportSite(…, { mode: "replace" }), which checks it like any bundle. The site's working copy and published pointer become that snapshot's again (cloudhal import --revert, RFC-0039). Revs only move forward: a node the site holds at the snapshot's rev or a later one, with other content, comes back at the rev after its current one, so a Studio tab or agent still holding a rev that node had gets a conflict instead of saving over it; a node whose content already is the snapshot's is left as it is. It lists the media those nodes need and carries the store's takedowns among them, so taken-down content stays down; a missing node body, or one that is not the node its snapshot ref names, is refused before anything is written.exportSite's output is unchanged. -
list_entrieswithorderByandlimitlets the store page (in SQL once the collection's list values are ready, RFC-0042 §4, #110) instead of loading every match. Arguments, cursors and output are unchanged; withoutorderBythe list keeps its last-updated order. -
render_page's long-line and input-label checks handle whitespace correctly again. A plain template literal ate the backslash, so the in-page script received
/s/and/s+/instead of/\s/and/\s+/. As a result, long-line counted the letter "s" as blank and every real space and indentation run as text. input-label splitaria-labelledbyon the letter "s" and reported labelled inputs as unlabelled. The check parts are nowString.rawtemplates. -
The admin Worker's minute tick gains a
lists.backfillsystem job (every five minutes): a budgetedrebuildListValuesfor listed collections that are new, changed, reset by a race or older than migration 0029, sized to the tick plan. Its declared cost (16 statements on the free plan, 150 on paid) is the run's statement budget, which the backfill enforces on every statement, discovery included, and each run starts at another waiting collection. On the paid plan it also removes rows of collections that list no fields any more, never those of a collection listed while it ran. -
New
cloudhal export(RFC-0039, #47) writes one site of the local project to a single.cloudhalfile. The file is a gzipped tar holding the site's content graph (nodes, snapshots and the node bodies they reference, as NDJSON parts), its media files and its takedown ledger, with every file's SHA-256 listed in the manifest. The flags:--out <file>names the file, and replaces a file already there once the new one is complete. By default the file is<site-slug>-<YYYYMMDD>.cloudhalin the current folder, dated by the newest timestamp among the rows it holds (deleting content alone does not move that date; the next edit or snapshot does). A file already at the default name is replaced only by the same archive; any other (a later edit the same day, other flags, a deletion) is left alone and the export asks for--out.--site <id>exports another site listed incloudhal.jsonsites.--site,--no-mediaand--latest-onlyare refused on every other command.--no-mediawrites the graph only.--latest-onlykeeps only the published...
Cloudhal 0.6.1
All @cloudhal/* packages and create-cloudhal at 0.6.1.
Changes
-
Gaps an agent hit building a site through MCP:
- Section-only theme blocks in article bodies are refused. A theme block type with
where: ["section"](saylink-cards) has no component in an article, and drew "unknown component" there.create_entry/update_entrynow refuse it inside a body```surfacefence (invalid_surface, with the component's path); primitives, built-in blocks and theme blocks that allowbodystay.get_catalog's "In article bodies" lists which blocks a body may use and which are page sections only. list_entrieslists every collection by default, not onlyposts: a site whose articles live inbloggot[].where/orderByneed the collection whose list fields they use; without one they are refused (invalid_queryatcollection) instead of ignored.long-linegives a text column one verdict. It counted characters over a rounded line count, so how full a paragraph's last line was decided whether it passed, and identical paragraphs in one column disagreed. It now counts the characters on each rendered line, leaves each paragraph's last line out, and pools the paragraphs of one width and font: one example per column, with its width and how many paragraphs it holds.- Blog starter: a primary surface
Buttonin an article body keeps itson-brandtext; the body's link colour no longer applies to buttons (it failed contrast at 2.5:1 light, 1.3:1 dark).
- Section-only theme blocks in article bodies are refused. A theme block type with
-
A collection's template may now sit on top of a page at its prefix:
set_collection_permalinkwithposts→/blog/{slug}no longer fails withpath_conflictbecause a page lives at/blog. That page is the collection's index page (RFC-0038), so only pages strictly under the prefix conflict, in the template check, its SQL race guard (noPageUnder) and the reverse check on page writes. Studio's "Create listing page" now works for a collection with its own permalink too. -
Theme scripts work on deployed sites, and
render_pagesays when they would not.- The
cloudhal()integration no longer lets Astro inline a component's<script>(under 4 KB) into the page, where the Site Worker'sscript-src 'self'blocked it: script chunks always ship as files (build.assetsInlineLimit), other assets keep the project's own limit. A theme that setvite.build.assetsInlineLimititself for this can drop it. cloudhal devsends the Site Worker's policy asContent-Security-Policy-Report-Onlyon pages without their own, so the browser console shows what a deploy would block without breaking Vite's dev client.render_pageandcloudhal rendergain two checks:surface-broken(a body```surfacefence the preview cannot draw, or a component with an unknown block type or props that do not fit, named with the problem) andscript-blocked(a script the page's CSP refused, inline or by URL; undercloudhal dev's report-only policy, what a deploy would block).
- The
-
Studio on a playground (RFC-0036): the Build page no longer says "This deployment can't reach Workers AI". It says Build with AI is available in your own install, since the playground leaves AI out, and links to deploying Cloudhal. Translations of the new sentence are AI drafts awaiting review.
Upgrade
npm install @cloudhal/cli@0.6.1 @cloudhal/core-ts@0.6.1 @cloudhal/astro-renderer@0.6.1 @cloudhal/cloudflare@0.6.1
npx cloudhal deployDocs: https://cloudhal.dev/docs/start/installation#upgrading
Cloudhal 0.6.0
All @cloudhal/* packages and create-cloudhal at 0.6.0.
Changes
- Collections stage 3 contract and pure core (RFC-0042 step 1, ADR-0044).
CollectionSettingsgains optionallistFields(up to 4 field keys),expiry({ field, action: "unlist" | "hide", afterMinutes? }) andstructuredData({ type: "Event" | "JobPosting" | "Product" | "Recipe", properties }, each property sourced bykind:field,entry,siteor a plain-texttextconstant);contentListsections gainwhere(up to 4FieldConditions) andorderBy(FieldOrder), both new incommon.schema.json. New@cloudhal/core-ts/structured-data: the closed preset table,checkStructuredData(save-time mapping check) andstructuredDataFor(an entry's JSON-LD, none when a required value is missing, capped at 32 KB).@cloudhal/core-ts/fieldsadds the listing evaluator (checkFieldQuery,parseFieldQuery,matchesFieldQuery,compareByField) and the derived expiry gate (fieldGateInstant,expiryInstant,effectiveUntil,dayStartIn). Nothing reads them yet: no behaviour change. - Collections stage 3 in the renderer (RFC-0042 step 3, ADR-0044). A collection's
expirygives every entry a derived gate (min(availability.until, field date + grace); a date ends at the next day's start in the site's zone), applied by both published sources at render time:unlistkeeps the entry's page but leavesentries(),allEntries(), term filters and archives, related posts, feeds, the sitemap,llms.txtand search (search rows'gateUntil);hideanswers 404 like an expiredavailability. The gate index adds every derived instant and every value instant of date/datetime list fields, so the edge cache expires there without a purge; previews ignore derived gates and say "Ended on …". New theme APIs:GraphSource.queryEntriesandqueryEntries(source, q)(./queries) list a collection by its list fields (where,orderBy,sort,filter,limit,offset,includeExpired) with one evaluator on live and snapshot sources;contentListEntries;GraphSource.routableEntries(entries with a public URL, unlisted included);withStructuredDataandentryStructuredData(./seo) replace the genericBlogPostingwith the collection's JSON-LD preset when the entry has every required value.ContentList.astrohonourswhere/orderBy, also for theme copies that pass a source's list.entryHeadis unchanged, so old themes keep today's output; the blog starter callswithStructuredDataandcontentListEntries. The site Worker'sGET /__cloudhal/featuresanswers{ "gates": 2 }, and writers (store, API, MCP, bundle import) refuse a new or changedexpirywith409 gates_unsupporteduntil it does (GateSupport.enforced(siteId, version),EXPIRY_GATES_VERSION). A live entry whose published change adds a derived gate or moves it earlier is never low risk inautomode (expiringEntries, auto-check codeexpires, for publishes and releases). Sites without the new settings render as before. - Collections stage 3 in the store and the API (RFC-0042 step 2, ADR-0044).
updateSitechecks the new collection settings when they change:listFieldsandexpiry.fieldname fields of the collection (unknown_field), the expiry field is a date or datetime field (invalid_expiry), and a structured-data preset passescheckStructuredData(structured_data_invalid; warnings are not refused). Unlisting, dropping or retyping a list field, or dropping a select option, thatcontentListsections still filter or sort by is refused aslist_field_in_usenaming the pages, kept true by a guard on the site CAS. Page writes check changedcontentListwhere/orderByagainst the collection's list fields (invalid_query, withsections/<i>/…paths). Field operations follow:rename_fieldrenames the field inlistFields,expiry, structured-data sources and sections' queries in phase A's batch (and back on revert);remove_fieldrefuses while any of them names the field;change_field_typerefuses when the new type would break them. NewqueryContentEntries(andapplyFieldQuery) filters and orders a collection's entries by list fields with the shared evaluator over resolved values.GET /api/sites/:id/entriestakeswhere(JSON),orderBy(JSON),limit(1–100) andcursor; withlimitit answers{ entries, total, nextCursor? }, without it the plain array as before. New error codesinvalid_query,structured_data_invalid,invalid_expiry(422) andlist_field_in_use(409). Sites without the new settings behave as before.
Patch Changes
- Typed fields follow-ups (RFC-0041). One asset check for field values,
fieldAssetIssuesin@cloudhal/core-ts/media(withfieldAssetRefsandnewFieldAssetRefs), now used by both the store's entry write and MCP'screate_entry/update_entryprecheck; same codes and messages. Machine translation now follows §5:get_translation_sourcereturns only the localized text and rich-text field values to translate (fields) and names the shared ones (sharedFields);save_translationtakes translatedfields, refuses shared ones in precheck withfield_not_localized(paths indetails.issues), and no longer copies the source's field values into a new translation in the same site. New helpers in@cloudhal/core-ts/fields:localizedFieldValues,translatableFieldValues,sharedFieldKeys,sharedFieldIssues. Studio's "write by hand" draft copies only localized field values. - Per-collection feeds (RFC-0042 step 6, ADR-0044). Each collection with a permalink prefix has an RSS feed at
<prefix>/rss.xml(and/<locale><prefix>/rss.xmlor the locale's domain): its newest listed entries, so expiry and time gates apply as in the site feed, with the same cache tag and edge-cache cap;feed: falseanswers 404. Thecloudhal()integration serves it from a middleware on every target, only where the theme's own routes answered 404, so a theme's own route wins and old theme copies gain the feeds; static builds write them becausestaticPaths().feedsnow lists every collection feed path for the starter's[...path]/rss.xmlroute. New theme APIcollectionFeedLink(site, collection, origin, locale?)(./seo,./collection-feeds) gives the<link rel="alternate">; the blog starter adds it on entry pages and on pages that list a collection. Previews now listcontentListsections withwhere/orderBythrough the same evaluator as the site (renderSectionHtml's new optionallistingargument, passed byrenderPagePreview), so an entry without the field's value no longer shows under "upcoming". New guide:docs/guides/structured-data.md. - Collections stage 3 in the renderer (RFC-0042 step 3, ADR-0044). A collection's
expirygives every entry a derived gate (min(availability.until, field date + grace); a date ends at the next day's start in the site's zone), applied by both published sources at render time:unlistkeeps the entry's page but leavesentries(),allEntries(), term filters and archives, related posts, feeds, the sitemap,llms.txtand search (search rows'gateUntil);hideanswers 404 like an expiredavailability. The gate index adds every derived instant and every value instant of date/datetime list fields, so the edge cache expires there without a purge; previews ignore derived gates and say "Ended on …". New theme APIs:GraphSource.queryEntriesandqueryEntries(source, q)(./queries) list a collection by its list fields (where,orderBy,sort,filter,limit,offset,includeExpired) with one evaluator on live and snapshot sources;contentListEntries;GraphSource.routableEntries(entries with a public URL, unlisted included);withStructuredDataandentryStructuredData(./seo) replace the genericBlogPostingwith the collection's JSON-LD preset when the entry has every required value.ContentList.astrohonourswhere/orderBy, also for theme copies that pass a source's list.entryHeadis unchanged, so old themes keep today's output; the blog starter callswithStructuredDataandcontentListEntries. The site Worker'sGET /__cloudhal/featuresanswers{ "gates": 2 }, and writers (store, API, MCP, bundle import) refuse a new or changedexpirywith409 gates_unsupporteduntil it does (GateSupport.enforced(siteId, version),EXPIRY_GATES_VERSION). A live entry whose published change adds a derived gate or moves it earlier is never low risk inautomode (expiringEntries, auto-check codeexpires, for publishes and releases). Sites without the new settings render as before.
Patch Changes
-
Collections stage 3, step 4 (RFC-0042 §7 / ADR-0044): MCP.
list_collectionsreturns, per collection, itslistFieldswith each field's type, operators (ops) and whether it sorts, and, for a collection with a JSON-LD preset,structuredData: the type, every path it offers (required ones, "at least one of" groups, defaults, the field types, entry and site properties, constants and vocabulary each takes) and the preset's warnings.update_collectiontakeslistFields,expiryandstructuredData(nullremoves one). The store's dry run checks them inprecheck, sounknown_field,invalid_expiry,structured_data_invalid,list_field_in_useandgates_unsupportedrefuse the call, with paths, before a person is asked. The approval says what changes in words ("Events leave listings, feeds, the sitemap and search (their pages stay) 1 hour after "Ends at" (endsAt); 2 published entries are already past it", "structured data: Event, maps startDate ← field startsAt, …", vocabulary warnings), and the answer lists the preset's warnings. Policy is unchanged (site:write, a person holdingsite:settingsapproves in every mode).list_entriestakeswhere,orderBy,limitandcursor, run by core-ts's shared evaluator (queryContentEntries); withlimitit an...
Cloudhal 0.5.0
All @cloudhal/* packages and create-cloudhal at 0.5.0.
Changes
-
Typed custom fields, step 1 (RFC-0041 / ADR-0043).
Site.collections.<key>.fieldstakes an ordered list of 1–30 field definitions in nine types (text,richText,number,boolean,date,datetime,url,image,select), discriminated bytype, each with per-locale labels,localizedand (exceptboolean)requiredToPublish. The new@cloudhal/core-ts/fieldssubpath (Worker-safe, eval-free) checks definitions (reserved keys, unique keys and options, 30 per collection, 200 per site, 256 KB of collection settings, the plain-edit guard), checks values through ADR-0030'svalidateProps(only changed values; unknown keys refused once a collection has definitions; 256 KB per entry), checksrequiredToPublish, emits the JSON Schema agents receive (strict-mode safe), and resolves values across translations (resolveFields). Nothing reads it yet: the store, API, MCP, Studio and renderer adopt it in later steps, so nothing changes for existing sites. -
Typed custom fields, step 3 (RFC-0041 §6 / ADR-0043): field operations.
runFieldOperation/continueFieldOperation/revertFieldOperation/fieldOperationStatusin@cloudhal/core-tsrunrename_field,change_field_type,set_field_localizedandremove_field: a dry run (entries touched, values moved, converted, cleared, removed, unchanged and unconvertible, per locale, with samples), abefore …snapshot whose id is the operation id, the new definition with itsmigrationmarker (phase A), entries rewritten in guarded batches of 200 that resume after an interruption (phase B), the marker dropped once nothing is pending (phase C) and anaftersnapshot. Only the draft graph changes. A type change follows a closed conversion table (convertFieldValue, exported from@cloudhal/core-ts/fields) and refuses values it cannot convert (field_values_unconvertible) unlessonInvalid: "clear". One operation per collection at a time (operation_in_progress). Reverting restores the earlier definition and the entries nobody edited since, and converts edited ones back.- Readers stay consistent mid-run:
resolveFieldsconverts values still in the old type, and a field being removed reads as gone; a person's entry write during an operation goes through the marker, so edits and the operation converge. - Translations: changing only non-localized field values of an original no longer marks its translations stale (their recorded source hash follows, in the same batch); operations keep fresh translations fresh.
- API:
POST /api/sites/:id/collections/:key/fields/operations(dryRun,?continue=<id>),GET …/:operationId,POST …/:operationId/revert(site:settingsandcontent:write), and the same under/api/adminwith the deployment token. - CLI:
cloudhal fields rename|retype|localize|remove|continue|revert [--dry-run] [--remote]; the dry run also lists theme files that read the field directly.
-
Typed custom fields, step 6 (RFC-0041 §9 / ADR-0043): renderer and themes. Additive only:
entry.fieldsstill means exactly what is stored, and themes copied before this release build and render as before (a frozen 0.4.0 theme joins the compat test).GraphSource.entryFields(entry, options?)on live and snapshot sources (andentryFields(source, entry, options?)from@cloudhal/astro-renderer/fieldsfor any source): an entry's typed fields as a theme draws them, by key in definition order. Values are resolved by the language rules (a translation's empty localized field reads the original's,fromOriginal; non-localized fields come from the translation group's original; language editions own their values), labelled in the page's language and made safe: rich text as HTML from the article body's markdown pipeline (raw HTML escaped,safeHrefon links), URLs throughsafeHref(dropped when unsafe), images as<img>attributes with the asset's alt in the page's language, everything else as formatted text. AlsorenderFields,hasShownFields, andresolveFields,formatFieldValue,fieldLabel,fieldHelp,optionLabelre-exported.formatFieldValue(value, def, { locale, defaultLocale?, timeZone?, words? })in@cloudhal/core-ts/fields: a value as plain text withIntl(numbers, dates that never shift a day, instants in their own zone, option labels as a list, yes/no words).EntryFields.astroin@cloudhal/astro-renderer/components: a plain, unstyled<dl class="ch-fields">of the fields that have a value, withdata-field/data-typehooks for themes; nothing at all when none has one.- Blog starter: articles of collections with fields show a "Details" list under the cover (English, Turkish and Japanese words). Entries of collections without definitions render byte for byte as before.
- New guide:
docs/guides/fields.md(defining fields in Studio, MCP, the API and the CLI; operations; languages; theme API; security).
-
Typed custom fields, step 2 (RFC-0041 / ADR-0043): the store and the API enforce field definitions.
- Site writes (
updateSite,createSite,PUT /api/sites/:id/collections/:key, dry run included) check definitions: reserved (field_key_reserved) and duplicate keys or options,minimumabovemaximum, 30 fields per collection and 200 per site (too_many_fields), 256 KB of collection settings (too_large). A plain edit never loses a value: changing a field'stype,localizedormultiple, or dropping a key or a select option that an entry still holds, is refused with409 field_change_needs_operation, naming the field operation to use. Other site writes do not re-run the checks. - Entry writes check only the field values that changed:
422 invalid_fields,unknown_field(a new key without a definition, once the collection has definitions; stored unknown keys are kept),field_not_localized(a translation's own copy of a non-localized value) andrequired_field(review or published without arequiredToPublishvalue; drafts save freely).fieldsis capped at 256 KB per entry in every collection when it changes (too_large). Image values must be image assets of the site, and image and rich-text asset references are recorded inasset_refsunderfields.<key>, so a used file cannot be deleted. Every refusal lists its problems with paths indetails.issues(fields/startsAt/at). - API inputs remove a key with
nullin a collection that has definitions. Collections without definitions keepfieldsas any JSON, exactly as before. - Search:
textandrichTextfields markedsearchableadd their words to the entry's search text at the next publish (off by default; sites without searchable fields index byte for byte as before). refuseFieldIssuesandfieldRefusalCodeare exported from@cloudhal/core-ts/fields.
- Site writes (
Patch Changes
-
Typed custom fields, step 6 (RFC-0041 §9 / ADR-0043): renderer and themes. Additive only:
entry.fieldsstill means exactly what is stored, and themes copied before this release build and render as before (a frozen 0.4.0 theme joins the compat test).GraphSource.entryFields(entry, options?)on live and snapshot sources (andentryFields(source, entry, options?)from@cloudhal/astro-renderer/fieldsfor any source): an entry's typed fields as a theme draws them, by key in definition order. Values are resolved by the language rules (a translation's empty localized field reads the original's,fromOriginal; non-localized fields come from the translation group's original; language editions own their values), labelled in the page's language and made safe: rich text as HTML from the article body's markdown pipeline (raw HTML escaped,safeHrefon links), URLs throughsafeHref(dropped when unsafe), images as<img>attributes with the asset's alt in the page's language, everything else as formatted text. AlsorenderFields,hasShownFields, andresolveFields,formatFieldValue,fieldLabel,fieldHelp,optionLabelre-exported.formatFieldValue(value, def, { locale, defaultLocale?, timeZone?, words? })in@cloudhal/core-ts/fields: a value as plain text withIntl(numbers, dates that never shift a day, instants in their own zone, option labels as a list, yes/no words).EntryFields.astroin@cloudhal/astro-renderer/components: a plain, unstyled<dl class="ch-fields">of the fields that have a value, withdata-field/data-typehooks for themes; nothing at all when none has one.- Blog starter: articles of collections with fields show a "Details" list under the cover (English, Turkish and Japanese words). Entries of collections without definitions render byte for byte as before.
- New guide:
docs/guides/fields.md(defining fields in Studio, MCP, the API and the CLI; operations; languages; theme API; security).
Patch Changes
-
Typed custom fields, step 4 (RFC-0041 §8 / ADR-0043): MCP.
list_collectionsreturns each collection's field definitions (fields) andfieldsSchema, the JSON Schema of an entry's values with labels and help in the askedlocale(default: the site's).create_entry/update_entrycheck field values inprecheckwith the store's checker (changed keys only, unknown keys, a translation's copy of a non-localized value,requiredToPublishfor review and published, image andasset:references), so a refused call never reaches a person; each issue keeps its path (fields/startsAt/at).update_entrytakesfieldsas a merge patch (nullremoves a key), and its approval names typed changes readably ("Starts at (startsAt) → 5 Nov 2026 19:30 Istanbul", option labels, values quoted and truncated).update_collectiontakesfields(the whole list) for free definition edits; changes that could lose values are refused withfield_change_needs_operation, naming th...
Cloudhal 0.4.0
All @cloudhal/* packages and create-cloudhal at 0.4.0.
Changes
- First-class collections, step 1 (RFC-0040 / ADR-0042).
Site.collections.<key>takes optionallabels(singular and plural per locale),icon,feed,sitemapandsortnext topermalink, with at most 50 collections per site.SiteGraphStore.listCollections(siteId)returns the registry: declared collections, those inferred from existing entries, andposts, with entry counts per status. The new@cloudhal/core-ts/collectionssubpath addscollectionLabelandcollectionSettingsfor themes. Nothing changes for existing sites. - First-class collections, step 2 (RFC-0040 / ADR-0042).
-
Store: the first entry of a new collection is refused (
path_conflict,reserved_path) when its default prefix/<collection>overlaps another collection's permalink, a taxonomy archive,/searchor a locale prefix. This closes the gap RFC-0038 left. A site update that declares a new collection runs the same check, and a declared collection that still has entries, or that a taxonomy names, cannot be undeclared (collection_in_use). -
API: new routes under
/api/sites/:id/collections:GET(content:read) lists the registry;PUT /:key { settings, rev }declares or changes a collection (site:settings), with?dryRun=1;DELETE /:key?rev=undeclares an empty one.
Creating an entry in a collection outside the registry is refused with
collection_undeclared(422). -
Renderer: RSS feeds leave out collections with
feed: false, andsitemap.xmlleaves out those withsitemap: false. The defaults are unchanged.collectionLabelandcollectionSettingsare re-exported from./routingfor themes. -
MCP: resetting a permalink with
set_collection_permalinkkeeps the collection declared. Agents get hints for the new error codes.
-
Patch Changes
- MCP collection tools (RFC-0040 step 4):
list_collections(content:read) lists every collection with its names, icon, entry counts by status, permalink, feed, sitemap and sort settings, and the taxonomies that apply.create_collectiondeclares a collection. It needs names in the default locale and optionally takes an icon, permalink, feed, sitemap and sort.update_collectionchanges a collection's names (merged by locale), icon, feed, sitemap or sort.- Both write tools need
site:writeand wait for a person in every mode, and that person must holdsite:settings. Overlaps, duplicates and missing names are refused before anyone is asked;dryRunchecks without asking. create_entrynow refuses a collection outside the registry (collection_undeclared) and points tocreate_collection. Collections that already have entries keep working.
Patch Changes
- First-class collections, step 2 (RFC-0040 / ADR-0042).
-
Store: the first entry of a new collection is refused (
path_conflict,reserved_path) when its default prefix/<collection>overlaps another collection's permalink, a taxonomy archive,/searchor a locale prefix. This closes the gap RFC-0038 left. A site update that declares a new collection runs the same check, and a declared collection that still has entries, or that a taxonomy names, cannot be undeclared (collection_in_use). -
API: new routes under
/api/sites/:id/collections:GET(content:read) lists the registry;PUT /:key { settings, rev }declares or changes a collection (site:settings), with?dryRun=1;DELETE /:key?rev=undeclares an empty one.
Creating an entry in a collection outside the registry is refused with
collection_undeclared(422). -
Renderer: RSS feeds leave out collections with
feed: false, andsitemap.xmlleaves out those withsitemap: false. The defaults are unchanged.collectionLabelandcollectionSettingsare re-exported from./routingfor themes. -
MCP: resetting a permalink with
set_collection_permalinkkeeps the collection declared. Agents get hints for the new error codes.
-
- Studio: first-class collections (RFC-0040 step 3).
- Sidebar: lists every collection of the site, declared ones and those that agents or imports filled. Each shows its name in Studio's language, its icon and its entry count, followed by Collections.
- Collections screen: shows each collection's address, entry count and whether it is named. It has a New collection dialog with plural and singular name, key, icon and address, and the API checks the address for overlaps.
- Collection list: each collection gets the Articles list (
/collections/<key>, where/articlesnow redirects). Category and tag columns and filters appear only where those taxonomies apply. - Collection settings: names in every site language, icon, RSS feed, sitemap, default order, and which categories and tags apply.
- Editor: its breadcrumb leads back to the entry's own collection.
- Elsewhere: the comments queue, the language review options and the taxonomy dialog cover every collection.
- Translations: new strings in all 19 languages; every language except English is an AI draft awaiting review.
Patch Changes
render_pageon Node: a Chrome that hangs or crashes on a cold start is launched once more with a fresh profile instead of failing the render.- Studio loads about 32 kB (gzip) less before the first screen: the notification panel's items, the agent-mode and elevation views and the taxonomy helpers now load when they are needed.
Upgrade
npm install @cloudhal/cli@0.4.0 @cloudhal/core-ts@0.4.0 @cloudhal/astro-renderer@0.4.0 @cloudhal/cloudflare@0.4.0
npx cloudhal deployDocs: https://cloudhal.dev/docs/start/installation#upgrading
Cloudhal 0.3.13
All @cloudhal/* packages and create-cloudhal at 0.3.13.
Changes
- Fix: sites whose theme was copied from the starter before 0.3.11 rendered empty pages (200 with no body). Their
EntryListcalls the renderer's old one-argumententryPath(entry)(and.map(entryPath)), which becameentryPath(site, entry)in 0.3.11.entryPathaccepts the old call again (deprecated; it ignores collection permalinks — pass the site to get them).
Upgrade
npm install @cloudhal/cli@0.3.13 @cloudhal/core-ts@0.3.13 @cloudhal/astro-renderer@0.3.13 @cloudhal/cloudflare@0.3.13
npx cloudhal deployDocs: https://cloudhal.dev/docs/start/installation#upgrading
Cloudhal 0.3.12
All @cloudhal/* packages and create-cloudhal at 0.3.12.
Changes
@cloudhal/core-ts/locale-paths: the locale URL helpers (localeRouting,localizedPath,localeUrl) move from the renderer into core-ts, browser-safe, withitemUrl(site, origin, item)for a page's or entry's public URL.@cloudhal/astro-renderer/localesre-exports them unchanged.- Studio shows changes that are not live yet: a badge beside Publish with the count ("2 changes", or "Not published" before the first publish) and a tooltip by kind ("3 changes aren't live yet · 2 articles and 1 page"). It comes from the new
GET /api/sites/:siteId/pending(site:read): counts of what a publish would add, change and remove, by kind; adding or removing a page counts once. Studio refreshes it after every write to the site, on window focus and every 30 s. - Studio links to the live site: a "View site" button in the top bar (also in the site switcher and the ⌘K palette), "Live at" and "Last published" on the overview, "View live" and "Copy link" on published pages and articles, and the full address under the slug and path fields ("Live at" when the live snapshot serves it, "Will be at" until then). After a site publish the toast offers "View site" and "Copy link", as do rollback and approval toasts. Marking a page or article published now says it goes live with the next site publish, with a "Publish site" action, instead of "Page published".
Upgrade
npm install @cloudhal/cli@0.3.12 @cloudhal/core-ts@0.3.12 @cloudhal/astro-renderer@0.3.12 @cloudhal/cloudflare@0.3.12
npx cloudhal deployDocs: https://cloudhal.dev/docs/start/installation#upgrading
Cloudhal 0.3.11
All @cloudhal/* packages and create-cloudhal at 0.3.11.
Changes
- RFC-0038 step 1:
Site.collections.<key>.permalinkin the contracts (a template,/blog/{slug}; v1 is fixed segments plus{slug}), andentryPath(site, entry)/entryAt(site, path)in@cloudhal/core-ts(also@cloudhal/core-ts/permalinks). Every entry URL in the renderer, starter, API, MCP server, CLI and Studio now comes from them; with nocollectionsset, every URL is the same as before. The renderer's one-argumententryPath(entry)is replaced by core'sentryPath(site, entry)(re-exported from@cloudhal/astro-renderer/routing); themes pass the site, orMediaContext.permalinks. - Collection permalinks, step 2 (RFC-0038 §3–§4): changing a collection's permalink in a site update adds a prefix redirect from the old URL prefix to the new one, in every locale and in the same batch as the site update. A template may not overlap another collection, a taxonomy archive, a page or a reserved path (
path_conflict/reserved_path), and pages and archives may not be created inside a configured prefix. The search index rewrites entry links on the next publish. - Collection permalinks, step 3 (RFC-0038 §5): Studio's Site settings has a Collections section, one row per collection with the URL prefix before
/{slug}and a real entry's address; saving goes through the site update, which writes the permanent redirect, and a path conflict shows on the row. The new MCP toolset_collection_permalink(site:write) moves a collection's URLs and always waits for a person, in every mode, with a prompt listing the old and new template, example entries' old and new URLs and the redirect;dryRun: truereturns that plan without changing anything, and overlaps are refused before anyone is asked.get_sitedocumentscollections.SiteGraphStore.updateSite(site, { dryRun: true })runs every check without writing. A menu item for a collection links to a published page at the collection's permalink prefix when there is one, before the page that lists the collection. The blog starter's README describes collection permalinks. Only someone who may change site settings (site:settings) approves it: tool policies take a newapproverScopes. - Fewer D1 round trips per admin API request. The session is cached in a signed cookie for 60 seconds, so a session revoked elsewhere can stay usable that long (ADR-0010 addendum); membership and roles still apply at once. A member and their site roles load in one query.
get-sessionis no longer rate limited, since the limiter's D1 read and write landed on every session check. Due comment purges are swept at most every 10 seconds per isolate instead of on every request. - Studio's images load faster and stop re-downloading. Preview URLs carry the file's hash (
?v=), so the browser keeps them for a day; without it the content route answersprivate, no-cacheand a 304 through the ETag, instead ofno-store. While an image loads, its tile shows the image's own colour and small blurred preview (computed at upload) instead of a spinner. - Studio switches screens faster: the signed-in session is kept between navigations (asked again after five minutes, on sign-out, on revoking sessions and on any 401) instead of being checked with the server on every click, and the pending screen shows after 150 ms instead of the router's one second, so a short wait no longer looks frozen.
Upgrade
npm install @cloudhal/cli@0.3.11 @cloudhal/core-ts@0.3.11 @cloudhal/astro-renderer@0.3.11 @cloudhal/cloudflare@0.3.11
npx cloudhal deployDocs: https://cloudhal.dev/docs/start/installation#upgrading
Cloudhal 0.3.10
All @cloudhal/* packages and create-cloudhal at 0.3.10.
Changes
- The site Worker's responses carry
Vary: Host, and redirect-host 301s never enter the edge cache (#62). Workers Cache keys by path, not hostname, so before this every host routed to one site Worker shared a cache entry per path: a--redirecthost's 301 could be served on the site's own domain (a redirect loop), and the sites of a multi-site Worker could serve each other's cached pages. cloudhal deployretries its post-deploy calls (push check, primary domain) for up to 90 seconds while they get 401: a Workers Builds deploy makes a new deployment token, and the edge can answer from the previous admin Worker version for a few seconds. Such a build used to fail after both Workers had deployed.
Upgrade
npm install @cloudhal/cli@0.3.10 @cloudhal/core-ts@0.3.10 @cloudhal/astro-renderer@0.3.10 @cloudhal/cloudflare@0.3.10
npx cloudhal deployDocs: https://cloudhal.dev/docs/start/installation#upgrading
Cloudhal 0.3.9
All @cloudhal/* packages and create-cloudhal at 0.3.9.
Changes
cloudhal domain set <host> --redirect www.<host>: hosts that answer with a 301 to the site's domain (path and query kept), routed to the site Worker as Custom Domains on the next deploy (#49).cloudhal domain remove redirectsdrops them.cloudhal domain set <host>also makes the host the local site's primary domain, so canonical links, the sitemap and feeds use it before the first deploy (#48).cloudhal snapshot --publishmakes no new snapshot when nothing changed since the live one; it says "nothing changed since " instead (#51).
Upgrade
npm install @cloudhal/cli@0.3.9 @cloudhal/core-ts@0.3.9 @cloudhal/astro-renderer@0.3.9 @cloudhal/cloudflare@0.3.9
npx cloudhal deployDocs: https://cloudhal.dev/docs/start/installation#upgrading