Skip to content

Releases: cloudhal-dev/cloudhal

Cloudhal 0.6.2

Choose a tag to compare

@github-actions github-actions released this 26 Oct 10:23
Immutable release. Only release title and notes can be modified.
17fd327

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, and entry_list_state, each listed collection's readiness (definition fingerprint, ready flag, backfill epoch and cursor). DDL only; existing data is untouched.

  • The ./testing subpath now ships. The published packages pointed ./testing at dist/src/testing.js and .d.ts, but the build left those files out, so importing @cloudhal/{storage,analytics,comments,schedules}/testing failed 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 own miniflare, 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 (behind cloudhal pull, the admin bundle route cloudhal push uses, and cloudhal import) now checks a bundle's project row before inserting it: it must be the site node's project (projectId), a valid Project, with its id and rev columns matching its body. Before, a bundle could plant any projects row.

  • Dependency patch updates (#115). @cloudhal/api depends on Better Auth 1.7.12 and on exactly the versions it uses itself: @better-auth/core and @better-auth/api-key 1.7.12, @better-auth/utils 0.5.0 and better-call 1.4.4. @cloudhal/cloudflare asks for zod ^4.6.7 (GHSA-mrhx-c7xc-m3w5 is fixed in 4.6.6). @cloudhal/core-ts and @cloudhal/astro-renderer use marked ^18.1.1. @cloudhal/core-ts and @cloudhal/schedules use temporal-polyfill 1.0.6. @cloudhal/astro-renderer uses vite ^8.3.5. The Studio shipped in @cloudhal/cli is 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's GET /entries?where=…, Studio's lists, MCP list_entries) answers where, orderBy, status, locale, limit and offset in SQL from the new entry_list_values side 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 listFields values as resolveFields reads 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. New rebuildListValues(sql, options) is the resumable, budgeted backfill (guarded per chunk, safe to run concurrently; budget.statements bounds every statement it issues, start rotates where a run begins, tidy removes rows of unlisted collections); updateSite, finished field operations and importSite run it for what they reset, and openLocalStore runs it on open. A field operation's revert resets every listed collection whose values its restored bodies change. deleteContentEntry behaves as before.

    revertTagsToTerms now 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 in edited, instead of restoring its earlier fields, status or collection.

  • importSite, used by cloudhal pull, by the admin bundle route cloudhal push uses, and by cloudhal 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: true it returns a bootstrap conflict and missing media instead of throwing them. A node body that is not JSON is refused as blob <hash> is not JSON instead of with the parser's message.

  • New restoreBundle(from, snapshot): a site bundle of one snapshot's nodes, rebuilt from the stored node bodies, for importSite(…, { 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_entries with orderBy and limit lets 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; without orderBy the 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 split aria-labelledby on the letter "s" and reported labelled inputs as unlabelled. The check parts are now String.raw templates.

  • The admin Worker's minute tick gains a lists.backfill system job (every five minutes): a budgeted rebuildListValues for 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 .cloudhal file. 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>.cloudhal in 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 in cloudhal.json sites. --site, --no-media and --latest-only are refused on every other command.
    • --no-media writes the graph only.
    • --latest-only keeps only the published...
Read more

Cloudhal 0.6.1

Choose a tag to compare

@github-actions github-actions released this 23 Oct 18:05
Immutable release. Only release title and notes can be modified.
e768209

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"] (say link-cards) has no component in an article, and drew "unknown component" there. create_entry/update_entry now refuse it inside a body ```surface fence (invalid_surface, with the component's path); primitives, built-in blocks and theme blocks that allow body stay. get_catalog's "In article bodies" lists which blocks a body may use and which are page sections only.
    • list_entries lists every collection by default, not only posts: a site whose articles live in blog got []. where/orderBy need the collection whose list fields they use; without one they are refused (invalid_query at collection) instead of ignored.
    • long-line gives 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 Button in an article body keeps its on-brand text; the body's link colour no longer applies to buttons (it failed contrast at 2.5:1 light, 1.3:1 dark).
  • A collection's template may now sit on top of a page at its prefix: set_collection_permalink with posts → /blog/{slug} no longer fails with path_conflict because 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_page says 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's script-src 'self' blocked it: script chunks always ship as files (build.assetsInlineLimit), other assets keep the project's own limit. A theme that set vite.build.assetsInlineLimit itself for this can drop it.
    • cloudhal dev sends the Site Worker's policy as Content-Security-Policy-Report-Only on pages without their own, so the browser console shows what a deploy would block without breaking Vite's dev client.
    • render_page and cloudhal render gain two checks: surface-broken (a body ```surface fence the preview cannot draw, or a component with an unknown block type or props that do not fit, named with the problem) and script-blocked (a script the page's CSP refused, inline or by URL; under cloudhal dev's report-only policy, what a deploy would block).
  • 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 deploy

Docs: https://cloudhal.dev/docs/start/installation#upgrading

Cloudhal 0.6.0

Choose a tag to compare

@github-actions github-actions released this 23 Oct 13:26
Immutable release. Only release title and notes can be modified.
689ff48

All @cloudhal/* packages and create-cloudhal at 0.6.0.

Changes

  • Collections stage 3 contract and pure core (RFC-0042 step 1, ADR-0044). CollectionSettings gains optional listFields (up to 4 field keys), expiry ({ field, action: "unlist" | "hide", afterMinutes? }) and structuredData ({ type: "Event" | "JobPosting" | "Product" | "Recipe", properties }, each property sourced by kind: field, entry, site or a plain-text text constant); contentList sections gain where (up to 4 FieldConditions) and orderBy (FieldOrder), both new in common.schema.json. New @cloudhal/core-ts/structured-data: the closed preset table, checkStructuredData (save-time mapping check) and structuredDataFor (an entry's JSON-LD, none when a required value is missing, capped at 32 KB). @cloudhal/core-ts/fields adds 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 expiry gives 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: unlist keeps the entry's page but leaves entries(), allEntries(), term filters and archives, related posts, feeds, the sitemap, llms.txt and search (search rows' gateUntil); hide answers 404 like an expired availability. 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.queryEntries and queryEntries(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); withStructuredData and entryStructuredData (./seo) replace the generic BlogPosting with the collection's JSON-LD preset when the entry has every required value. ContentList.astro honours where/orderBy, also for theme copies that pass a source's list. entryHead is unchanged, so old themes keep today's output; the blog starter calls withStructuredData and contentListEntries. The site Worker's GET /__cloudhal/features answers { "gates": 2 }, and writers (store, API, MCP, bundle import) refuse a new or changed expiry with 409 gates_unsupported until 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 in auto mode (expiringEntries, auto-check code expires, 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). updateSite checks the new collection settings when they change: listFields and expiry.field name fields of the collection (unknown_field), the expiry field is a date or datetime field (invalid_expiry), and a structured-data preset passes checkStructuredData (structured_data_invalid; warnings are not refused). Unlisting, dropping or retyping a list field, or dropping a select option, that contentList sections still filter or sort by is refused as list_field_in_use naming the pages, kept true by a guard on the site CAS. Page writes check changed contentList where/orderBy against the collection's list fields (invalid_query, with sections/<i>/… paths). Field operations follow: rename_field renames the field in listFields, expiry, structured-data sources and sections' queries in phase A's batch (and back on revert); remove_field refuses while any of them names the field; change_field_type refuses when the new type would break them. New queryContentEntries (and applyFieldQuery) filters and orders a collection's entries by list fields with the shared evaluator over resolved values. GET /api/sites/:id/entries takes where (JSON), orderBy (JSON), limit (1–100) and cursor; with limit it answers { entries, total, nextCursor? }, without it the plain array as before. New error codes invalid_query, structured_data_invalid, invalid_expiry (422) and list_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, fieldAssetIssues in @cloudhal/core-ts/media (with fieldAssetRefs and newFieldAssetRefs), now used by both the store's entry write and MCP's create_entry/update_entry precheck; same codes and messages. Machine translation now follows §5: get_translation_source returns only the localized text and rich-text field values to translate (fields) and names the shared ones (sharedFields); save_translation takes translated fields, refuses shared ones in precheck with field_not_localized (paths in details.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.xml or 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: false answers 404. The cloudhal() 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 because staticPaths().feeds now lists every collection feed path for the starter's [...path]/rss.xml route. New theme API collectionFeedLink(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 list contentList sections with where/orderBy through the same evaluator as the site (renderSectionHtml's new optional listing argument, passed by renderPagePreview), 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 expiry gives 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: unlist keeps the entry's page but leaves entries(), allEntries(), term filters and archives, related posts, feeds, the sitemap, llms.txt and search (search rows' gateUntil); hide answers 404 like an expired availability. 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.queryEntries and queryEntries(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); withStructuredData and entryStructuredData (./seo) replace the generic BlogPosting with the collection's JSON-LD preset when the entry has every required value. ContentList.astro honours where/orderBy, also for theme copies that pass a source's list. entryHead is unchanged, so old themes keep today's output; the blog starter calls withStructuredData and contentListEntries. The site Worker's GET /__cloudhal/features answers { "gates": 2 }, and writers (store, API, MCP, bundle import) refuse a new or changed expiry with 409 gates_unsupported until 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 in auto mode (expiringEntries, auto-check code expires, 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_collections returns, per collection, its listFields with 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_collection takes listFields, expiry and structuredData (null removes one). The store's dry run checks them in precheck, so unknown_field, invalid_expiry, structured_data_invalid, list_field_in_use and gates_unsupported refuse 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 holding site:settings approves in every mode).
    • list_entries takes where, orderBy, limit and cursor, run by core-ts's shared evaluator (queryContentEntries); with limit it an...
Read more

Cloudhal 0.5.0

Choose a tag to compare

@github-actions github-actions released this 23 Oct 05:34
Immutable release. Only release title and notes can be modified.
01a685b

All @cloudhal/* packages and create-cloudhal at 0.5.0.

Changes

  • Typed custom fields, step 1 (RFC-0041 / ADR-0043). Site.collections.<key>.fields takes an ordered list of 1–30 field definitions in nine types (text, richText, number, boolean, date, datetime, url, image, select), discriminated by type, each with per-locale labels, localized and (except boolean) requiredToPublish. The new @cloudhal/core-ts/fields subpath (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's validateProps (only changed values; unknown keys refused once a collection has definitions; 256 KB per entry), checks requiredToPublish, 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 / fieldOperationStatus in @cloudhal/core-ts run rename_field, change_field_type, set_field_localized and remove_field: a dry run (entries touched, values moved, converted, cleared, removed, unchanged and unconvertible, per locale, with samples), a before … snapshot whose id is the operation id, the new definition with its migration marker (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 an after snapshot. 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) unless onInvalid: "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: resolveFields converts 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:settings and content:write), and the same under /api/admin with 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.fields still 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 (and entryFields(source, entry, options?) from @cloudhal/astro-renderer/fields for 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, safeHref on links), URLs through safeHref (dropped when unsafe), images as <img> attributes with the asset's alt in the page's language, everything else as formatted text. Also renderFields, hasShownFields, and resolveFields, formatFieldValue, fieldLabel, fieldHelp, optionLabel re-exported.
    • formatFieldValue(value, def, { locale, defaultLocale?, timeZone?, words? }) in @cloudhal/core-ts/fields: a value as plain text with Intl (numbers, dates that never shift a day, instants in their own zone, option labels as a list, yes/no words).
    • EntryFields.astro in @cloudhal/astro-renderer/components: a plain, unstyled <dl class="ch-fields"> of the fields that have a value, with data-field / data-type hooks 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, minimum above maximum, 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's type, localized or multiple, or dropping a key or a select option that an entry still holds, is refused with 409 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) and required_field (review or published without a requiredToPublish value; drafts save freely). fields is 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 in asset_refs under fields.<key>, so a used file cannot be deleted. Every refusal lists its problems with paths in details.issues (fields/startsAt/at).
    • API inputs remove a key with null in a collection that has definitions. Collections without definitions keep fields as any JSON, exactly as before.
    • Search: text and richText fields marked searchable add 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).
    • refuseFieldIssues and fieldRefusalCode are exported from @cloudhal/core-ts/fields.

Patch Changes

  • Typed custom fields, step 6 (RFC-0041 §9 / ADR-0043): renderer and themes. Additive only: entry.fields still 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 (and entryFields(source, entry, options?) from @cloudhal/astro-renderer/fields for 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, safeHref on links), URLs through safeHref (dropped when unsafe), images as <img> attributes with the asset's alt in the page's language, everything else as formatted text. Also renderFields, hasShownFields, and resolveFields, formatFieldValue, fieldLabel, fieldHelp, optionLabel re-exported.
    • formatFieldValue(value, def, { locale, defaultLocale?, timeZone?, words? }) in @cloudhal/core-ts/fields: a value as plain text with Intl (numbers, dates that never shift a day, instants in their own zone, option labels as a list, yes/no words).
    • EntryFields.astro in @cloudhal/astro-renderer/components: a plain, unstyled <dl class="ch-fields"> of the fields that have a value, with data-field / data-type hooks 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_collections returns each collection's field definitions (fields) and fieldsSchema, the JSON Schema of an entry's values with labels and help in the asked locale (default: the site's).
    • create_entry / update_entry check field values in precheck with the store's checker (changed keys only, unknown keys, a translation's copy of a non-localized value, requiredToPublish for review and published, image and asset: references), so a refused call never reaches a person; each issue keeps its path (fields/startsAt/at). update_entry takes fields as a merge patch (null removes 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_collection takes fields (the whole list) for free definition edits; changes that could lose values are refused with field_change_needs_operation, naming th...
Read more

Cloudhal 0.4.0

Choose a tag to compare

@github-actions github-actions released this 22 Oct 21:07
Immutable release. Only release title and notes can be modified.
bcd6105

All @cloudhal/* packages and create-cloudhal at 0.4.0.

Changes

  • First-class collections, step 1 (RFC-0040 / ADR-0042). Site.collections.<key> takes optional labels (singular and plural per locale), icon, feed, sitemap and sort next to permalink, with at most 50 collections per site. SiteGraphStore.listCollections(siteId) returns the registry: declared collections, those inferred from existing entries, and posts, with entry counts per status. The new @cloudhal/core-ts/collections subpath adds collectionLabel and collectionSettings for 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, /search or 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, and sitemap.xml leaves out those with sitemap: false. The defaults are unchanged. collectionLabel and collectionSettings are re-exported from ./routing for themes.

    • MCP: resetting a permalink with set_collection_permalink keeps 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_collection declares a collection. It needs names in the default locale and optionally takes an icon, permalink, feed, sitemap and sort.
    • update_collection changes a collection's names (merged by locale), icon, feed, sitemap or sort.
    • Both write tools need site:write and wait for a person in every mode, and that person must hold site:settings. Overlaps, duplicates and missing names are refused before anyone is asked; dryRun checks without asking.
    • create_entry now refuses a collection outside the registry (collection_undeclared) and points to create_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, /search or 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, and sitemap.xml leaves out those with sitemap: false. The defaults are unchanged. collectionLabel and collectionSettings are re-exported from ./routing for themes.

    • MCP: resetting a permalink with set_collection_permalink keeps 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 /articles now 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_page on 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 deploy

Docs: https://cloudhal.dev/docs/start/installation#upgrading

Cloudhal 0.3.13

Choose a tag to compare

@github-actions github-actions released this 22 Oct 14:08
Immutable release. Only release title and notes can be modified.
33efc16

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 EntryList calls the renderer's old one-argument entryPath(entry) (and .map(entryPath)), which became entryPath(site, entry) in 0.3.11. entryPath accepts 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 deploy

Docs: https://cloudhal.dev/docs/start/installation#upgrading

Cloudhal 0.3.12

Choose a tag to compare

@github-actions github-actions released this 22 Oct 13:03
Immutable release. Only release title and notes can be modified.
16bc60c

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, with itemUrl(site, origin, item) for a page's or entry's public URL. @cloudhal/astro-renderer/locales re-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 deploy

Docs: https://cloudhal.dev/docs/start/installation#upgrading

Cloudhal 0.3.11

Choose a tag to compare

@github-actions github-actions released this 22 Oct 08:56
Immutable release. Only release title and notes can be modified.
0e07b77

All @cloudhal/* packages and create-cloudhal at 0.3.11.

Changes

  • RFC-0038 step 1: Site.collections.<key>.permalink in the contracts (a template, /blog/{slug}; v1 is fixed segments plus {slug}), and entryPath(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 no collections set, every URL is the same as before. The renderer's one-argument entryPath(entry) is replaced by core's entryPath(site, entry) (re-exported from @cloudhal/astro-renderer/routing); themes pass the site, or MediaContext.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 tool set_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: true returns that plan without changing anything, and overlaps are refused before anyone is asked. get_site documents collections. 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 new approverScopes.
  • 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-session is 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 answers private, no-cache and a 304 through the ETag, instead of no-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 deploy

Docs: https://cloudhal.dev/docs/start/installation#upgrading

Cloudhal 0.3.10

Choose a tag to compare

@github-actions github-actions released this 21 Oct 21:32
Immutable release. Only release title and notes can be modified.
d285dd9

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 --redirect host'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 deploy retries 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 deploy

Docs: https://cloudhal.dev/docs/start/installation#upgrading

Cloudhal 0.3.9

Choose a tag to compare

@github-actions github-actions released this 21 Oct 19:54
Immutable release. Only release title and notes can be modified.
03d556e

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 redirects drops 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 --publish makes 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 deploy

Docs: https://cloudhal.dev/docs/start/installation#upgrading