- [Minor Changes](#minor-changes)
- [Patch Changes](#patch-changes)#6771 95758a0 Thanks @wicksipedia! - ✨ Visual editing for Astro — without React.
TinaCMS visual editing previously required useTina(), a React hook that subscribes to admin postMessages and re-renders the page tree. That made it a hard sell for Astro: the framework is built around shipping zero JS by default, and the existing examples/astro/kitchen-sink worked around the React requirement by hydrating React inside the editor iframe — exactly the pattern Astro authors avoid.
This release ships a vanilla-JS bridge that brings the same click-to-focus, live-update, and form-syncing UX to Astro components, Hugo templates, plain HTML — anything that can emit a data-tina-form payload per query.
New package: @tinacms/bridge
A ~2 KB gzipped, zero-dependency ESM bundle that speaks the existing TinaCMS admin postMessage protocol. No React in the page tree, no client islands, no hydration cost outside the editor iframe.
Astro projects install @tinacms/astro instead and the bundled integration's middleware auto-injects everything on edit-mode responses. Direct @tinacms/bridge consumption is for non-Astro frontends:
<head>
<div
data-tina-form='{"id":"…","query":"…","variables":{},"data":{}}'
hidden
></div>
<script type="module">
import { init } from "/_tina/bridge.js";
init();
</script>
</head>The bridge submodules:
init() — top-level entry. Detects iframe embedding, registers all [data-tina-form] payloads with the admin (with retry, since the bridge boots faster than the admin's listener), wires data updates and click-to-focus.refreshForms() — re-scans the DOM after soft navigations (Astro view transitions, Turbo, htmx). Posts close for forms that left and open for forms that appeared.tinaField() — framework-free field-id helper, identical API to tinacms/dist/react's export. Use on any element to make it click-to-edit.@tinacms/bridge/preview — server-side helper for non-React frameworks. readOverlay(request, queryId) returns the unsaved form data the admin is editing, so per-route refresh endpoints can re-render with overlay data on every keystroke.How edits flow without re-rendering React
The bridge takes a soft-refresh approach instead of in-place reconciliation. Mark editable regions with data-tina-island="<endpoint-url>"; on every form change the bridge POSTs the current overlay to that endpoint, the server renders the matching component to an HTML fragment, and the bridge swaps it into the live DOM. Per-island scoped — editing the hero refetches only the hero, not the whole page. The transport is JSON-over-POST so UTF-8 (em-dashes, smart quotes, emoji) and large rich-text bodies round-trip without size or charset limits.
The protocol stays stateless — admin pushes already-resolved data to the bridge, bridge forwards it to the island endpoint, endpoint reads it via readOverlay() instead of hitting the canonical content store. Works identically against self-hosted Tina, TinaCloud, or any GraphQL endpoint. No backend changes shipped.
tinacms: framework-free tinaField subpath
tinaField() was already pure — just reads _content_source metadata. It's now exported from tinacms/tina-field as a standalone module so non-React frontends can import it without pulling React (and Plate, and dnd-kit, and ~50 other React deps) into their bundle. The existing tinacms/dist/react re-export keeps the public API stable.
Reference example: examples/astro/visual-editing
A new Astro 5 example that mirrors examples/astro/kitchen-sink field-for-field — same six collections (Tag, Author, Global, Post, Blog, Page), same shared content via localContentPath, same eight routes — but rendered with pure Astro components instead of React islands. Includes:
@tinacms/astro package's TinaMarkdown — a vanilla Astro rich-text renderer that walks the Plate AST Tina returns, dispatches custom MDX components (NewsletterSignup, BlockQuote, DateTime, code blocks) by name to authored Astro components — the same components map shape as TinaMarkdown from tinacms/dist/rich-text, but emitting Astro markupsrc/pages/tina-island/[name].ts backed by a registry in src/lib/islands.ts. The endpoint uses Astro's experimental_AstroContainer to render the matching component as a fragment-only response. Adding a new editable region is one entry in the registryrequestWithMetadata() helper wrapping every data load so the same code path runs in production (no overlay → real fetch) and inside the editor (overlay → use the bridge payload). Production builds ship zero bridge JS to non-admin visitorsWhy this matters for the Astro community
Astro is the second-most-starred meta-framework on GitHub and grew specifically because authors care about runtime cost. Every previous attempt to integrate a React-based CMS into Astro carried the same caveat: "but you'll need to ship React for editing." That caveat is now gone. The bridge is the smallest piece of JS that can deliver Tina's full editing experience — click to focus, live preview as you type, click-to-edit overlays — to a framework whose audience explicitly didn't sign up for React.
Known content-shape note
For nested MDX components in rich-text bodies (e.g. <NewsletterSignup> inside a post's _body) to render via the Astro renderer instead of as raw HTML, the content needs to be authored through the Tina editor — which inserts them as MDX templates that Tina parses into mdxJsxFlowElement nodes. Hand-authored <Component> syntax in the markdown source is currently parsed as html by Tina's MDX layer; same behaviour as the React renderer. Worth flagging up-front for anyone migrating existing markdown content.
Soft-navigation support: refreshForms()
init() scans [data-tina-form] elements once on first load and captures the resulting set in closure. Sites using Astro's <ClientRouter /> (or any view-transitions setup that swaps the DOM without a full reload) would post the first page's forms to the admin and never refresh them — navigating between docs inside the editor iframe left the sidebar showing the previous page's form.
refreshForms() re-scans the live DOM, diffs against the previously-mounted set, and posts close for forms that disappeared and open (with the same retry-until-acked behaviour as init) for forms that appeared. The one-time global listeners — click capture, the updateData ack handler, the beforeunload close — stay bound across refreshes, so calling it on every navigation is cheap and idempotent. The Astro integration wires it to astro:page-load automatically.
Sticky edit-mode
A __tina_edit session cookie (SameSite=Strict, gated on Sec-Fetch-Dest: iframe) keeps the iframe in edit mode across in-iframe link clicks — without it, clicking a link inside the preview drops the /admin/ Referer and the next request falls out of edit mode. Top-level visitors never get edit mode because the dest check fails before the cookie is consulted, so production HTML is unaffected.
Out of scope (follow-ups)
#6765 9e7eba9 Thanks @kulesy! - Forward the editor's current branch to the TinaCloud assets-api on every cloud media call, and fix staging URL handling for multi-segment branches
TinaMediaStore now appends ?branch=<encodedBranch> to its upload_url, list, and delete requests so that — once the assets-api opts an app into branch-aware media — uploads, listings, and deletions are scoped to the branch the editor is on, instead of always hitting the production branch. The branch is read from Client.branch (already URL-encoded) and decoded then re-encoded at the use site to avoid double-encoding.
The query parameter is ignored by assets-api versions that do not parse it, so this change is safe to deploy ahead of the server-side rollout. Local mode is unaffected.
@tinacms/graphql's media URL resolver now formats staging URLs as /__staging/<branch>/__file/<path> instead of /__staging/<encoded-branch>/<path>. The previous form broke for branches containing / (e.g. feat/my-branch) because CloudFront decodes paths before downstream components see them, so the S3 write key (with a literal %2F) wouldn't match the decoded read path. The __file delimiter lets the branch contribute its natural / segments while still marking where the file path begins.
Note: staging URLs produced by @tinacms/graphql@2.3.0–2.3.1 use the old format and will not round-trip through this version's resolveMediaCloudToRelative. Branch-aware media is gated server-side and has not been enabled for any tenant yet, so no persisted data is expected to be affected — but if you turned it on for testing, regenerate the affected field values from the editor after upgrading.
After a successful cloud upload TinaMediaStore.persist() now resolves its return value from the assets-api list endpoint instead of constructing each Media.src locally — the server is the source of truth for the canonical URL (including the staging-branch path and per-stage CDN host). The MediaStore.persist() contract is preserved, so the returned items still flow through the media manager and the image-field drop handler.
Also reserves an optional rename?(from, to) hook on the MediaStore interface as a future extension point — no implementation yet.
#6694 723632b Thanks @alhafoudh! - Fix crash in getFieldGroup when editing deeply nested rich-text fields (3+ levels) with templates. The method used findIndex which always searched from the start of the path array, causing it to resolve the wrong "children"/"props" segments on recursive calls. Replaced with indexOf searching from the current position, and added a null guard for graceful fallback on malformed content.
Updated dependencies [95758a0]:
Last Edited: May 12, 2026