Skip to content

[pull] main from tinacms:main#215

Merged
pull[bot] merged 1 commit into
code:mainfrom
tinacms:main
May 12, 2026
Merged

[pull] main from tinacms:main#215
pull[bot] merged 1 commit into
code:mainfrom
tinacms:main

Conversation

@pull
Copy link
Copy Markdown

@pull pull Bot commented May 12, 2026

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @tinacms/astro@0.2.0

### Minor Changes

- [#6771](#6771)
[`95758a0`](95758a0)
Thanks [@wicksipedia](https://github.com/wicksipedia)! - ✨ **New
package: `@tinacms/astro`** — the one-stop integration for using TinaCMS
with Astro.

    ```bash
    pnpm add @tinacms/astro
    ```

Bundles the rich-text renderer and re-exports the framework-agnostic
bridge under one install. `@tinacms/bridge` stays publishable on its own
for non-Astro frontends (coming soon); Astro projects only need
`@tinacms/astro`.

    **What's exported**

| Subpath | What it gives you |
| ----------------------------------- |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
|
| `@tinacms/astro` | `requestWithMetadata`, `tinaField`, `QueryResult`,
and the rich-text types |
| `@tinacms/astro/TinaMarkdown.astro` | `<TinaMarkdown content
components />` — the rich-text renderer (import via subpath so Astro's
check sees a real `.astro` component) |
| `@tinacms/astro/integration` | `tina()` integration — auto-wires the
middleware and bridge route so `requestWithMetadata()` works without
threading `Astro.request` or writing wiring components |
| `@tinacms/astro/TinaIsland.astro` | `<TinaIsland name wrapper params
/>` — marker wrapper for an editable region |
| `@tinacms/astro/types` | `TinaRichTextContent`, `CustomComponentsMap`,
`TinaRichTextNode`, `MdxElement`, `TextElement`, etc. |
| `@tinacms/astro/sanitize` | `sanitizeHref` / `sanitizeImageSrc` for
CMS-supplied URLs |
| `@tinacms/astro/bridge` | `init`, `refreshForms`, and the rest of
`@tinacms/bridge` |
| `@tinacms/astro/tina-field` | `tinaField()` helper for
`data-tina-field` markers |
| `@tinacms/astro/is-edit-mode` | `isEditMode(request)` — server-side
admin-iframe detection |
| `@tinacms/astro/experimental` | `experimental_createIslandRoute()` —
opt-in helper for the dynamic `/tina-island/[name]` endpoint |

    **Usage**

    ```astro
    ---
    import TinaMarkdown from '@tinacms/astro/TinaMarkdown.astro';
    import { requestWithMetadata, tinaField } from '@tinacms/astro';
    import client from '../tina/__generated__/client';
    import { customComponents } from '../components/markdown';

    const post = await requestWithMetadata(
      client.queries.post({ relativePath: 'hello.md' }),
    );
    ---
    <div data-tina-field={tinaField(post.data.post, '_body')}>
<TinaMarkdown content={post.data.post._body}
components={customComponents} />
    </div>
    ```

Add `tina()` from `@tinacms/astro/integration` to your
`astro.config.mjs` and the middleware auto-injects the bridge script +
per-form payloads on edit-mode requests. Production HTML is
byte-identical to a Tina-free Astro app.

The renderer mirrors the React `TinaMarkdown` from
`tinacms/dist/rich-text` — same `content` prop, same `components` map
shape — but emits pure HTML with no React in the page tree. Custom MDX
components register by name (`mdxJsxFlowElement` / `mdxJsxTextElement`);
default tags (`p`, `h1`, `a`, etc.) can be overridden by registering
them on the same map.

    **Peer deps**

- `astro >=5.0.0` — uses Astro's container API for islands and ships
`.astro` source files for the consumer's Astro pipeline to compile.

### Patch Changes

- Updated dependencies
\[[`95758a0`](95758a0)]:
    -   @tinacms/bridge@0.2.0

## @tinacms/bridge@0.2.0

### Minor Changes

- [#6771](#6771)
[`95758a0`](95758a0)
Thanks [@wicksipedia](https://github.com/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:

    ```html
    <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:

- The **`@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 markup
- An island-refresh pattern: one dynamic endpoint at
`src/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
registry
- Multi-form pages: layout fetches global, route fetches its primary
collection, both register independently — admin shows the right form
based on which marked element you click
- A **`requestWithMetadata()`** 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 visitors

    **Why 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)**

- Hugo / Eleventy adapters using the same bridge — the contract is
framework-free, just needs an integration guide
- TinaCloud overlay channel — not needed; the stateless POST protocol
works against any backend

## @tinacms/cli@2.3.0

### Minor Changes

- [#6738](#6738)
[`4d0c37a`](4d0c37a)
Thanks [@joshbermanssw](https://github.com/joshbermanssw)! - Stop
writing generated files (`_schema.json`, `_graphql.json`,
`_lookup.json`, `tina-lock.json`) to the content repo when
`localContentPath` is set. Generated files now live only in the
generator repo's `tina/__generated__/`. The content repo is no longer
required to contain a `tina/` folder. `FilesystemBridge.get` / `put` /
`delete` now route `tina/__generated__/` and `.tina/__generated__/`
paths to `rootPath` (the generator) instead of `outputPath` (the content
root). Closes
[tinacms/tinacloud#3295](https://github.com/tinacms/tinacloud/issues/3295).

    ### ⚠️ Rollout gate

**This release must not be promoted to the `@latest` dist-tag until
TinaCloud prod has deployed
[tinacms/tinacloud#3403](https://github.com/tinacms/tinacloud/issues/3403).**
Pre-#3403 TinaCloud reads `tina-lock.json` from the content repo on
generator pushes; shipping this change before the server-side fix breaks
every existing multi-repo user's indexing.

    ### Migration notes for existing multi-repo projects

    After upgrading (and once TinaCloud prod is on #3403):

- **Stale `tina/` folder in your content repo.** Pre-upgrade builds
committed `tina/__generated__/*` and `tina/tina-lock.json` to the
content repo. Nothing updates or reads those files any more. They are
safe — and recommended — to delete from the content repo in a single
cleanup commit.
- **`ConfigManager.generatedFolderPathContentRepo` is removed.** If any
custom CLI code, plugins, or scripts referenced this field, they will
fail at type-check or runtime. Use `generatedFolderPath` — it has always
been the generator-relative path.
- **`ConfigManager.getTinaFolderPath` no longer accepts an
`isContentRoot` option.** The content root never needs a `tina/` folder
now, so the option was removed. If any custom code called
`getTinaFolderPath(path, { isContentRoot: true })`, drop the second
argument.
- **`FilesystemBridge` behavior change for `tina/__generated__/`
paths.** In multi-repo setups, bridge reads/writes of paths under
`tina/__generated__/` or `.tina/__generated__/` now resolve against the
generator (`rootPath`) rather than the content repo (`outputPath`). If
you have custom bridge subclasses or code that relied on these paths
resolving to the content repo, update it.
- **Generated `client.ts` / `database-client.ts` now import `./types`
extensionless** (was `./types.ts`) for TypeScript projects. Avoids
requiring `allowImportingTsExtensions: true` in consumer tsconfigs,
which broke the build under Next.js 15.5+ defaults. JS projects still
import `./types.js` (Node ESM requires the extension).

### Patch Changes

- Updated dependencies
\[[`723632b`](723632b),
[`95758a0`](95758a0),
[`eafb1ff`](eafb1ff),
[`4d0c37a`](4d0c37a),
[`9e7eba9`](9e7eba9)]:
    -   tinacms@3.8.0
    -   @tinacms/graphql@2.4.0
    -   @tinacms/metrics@2.1.0
    -   @tinacms/app@2.4.7
    -   @tinacms/search@1.2.14

## @tinacms/graphql@2.4.0

### Minor Changes

- [#6738](#6738)
[`4d0c37a`](4d0c37a)
Thanks [@joshbermanssw](https://github.com/joshbermanssw)! - Stop
writing generated files (`_schema.json`, `_graphql.json`,
`_lookup.json`, `tina-lock.json`) to the content repo when
`localContentPath` is set. Generated files now live only in the
generator repo's `tina/__generated__/`. The content repo is no longer
required to contain a `tina/` folder. `FilesystemBridge.get` / `put` /
`delete` now route `tina/__generated__/` and `.tina/__generated__/`
paths to `rootPath` (the generator) instead of `outputPath` (the content
root). Closes
[tinacms/tinacloud#3295](https://github.com/tinacms/tinacloud/issues/3295).

    ### ⚠️ Rollout gate

**This release must not be promoted to the `@latest` dist-tag until
TinaCloud prod has deployed
[tinacms/tinacloud#3403](https://github.com/tinacms/tinacloud/issues/3403).**
Pre-#3403 TinaCloud reads `tina-lock.json` from the content repo on
generator pushes; shipping this change before the server-side fix breaks
every existing multi-repo user's indexing.

    ### Migration notes for existing multi-repo projects

    After upgrading (and once TinaCloud prod is on #3403):

- **Stale `tina/` folder in your content repo.** Pre-upgrade builds
committed `tina/__generated__/*` and `tina/tina-lock.json` to the
content repo. Nothing updates or reads those files any more. They are
safe — and recommended — to delete from the content repo in a single
cleanup commit.
- **`ConfigManager.generatedFolderPathContentRepo` is removed.** If any
custom CLI code, plugins, or scripts referenced this field, they will
fail at type-check or runtime. Use `generatedFolderPath` — it has always
been the generator-relative path.
- **`ConfigManager.getTinaFolderPath` no longer accepts an
`isContentRoot` option.** The content root never needs a `tina/` folder
now, so the option was removed. If any custom code called
`getTinaFolderPath(path, { isContentRoot: true })`, drop the second
argument.
- **`FilesystemBridge` behavior change for `tina/__generated__/`
paths.** In multi-repo setups, bridge reads/writes of paths under
`tina/__generated__/` or `.tina/__generated__/` now resolve against the
generator (`rootPath`) rather than the content repo (`outputPath`). If
you have custom bridge subclasses or code that relied on these paths
resolving to the content repo, update it.
- **Generated `client.ts` / `database-client.ts` now import `./types`
extensionless** (was `./types.ts`) for TypeScript projects. Avoids
requiring `allowImportingTsExtensions: true` in consumer tsconfigs,
which broke the build under Next.js 15.5+ defaults. JS projects still
import `./types.js` (Node ESM requires the extension).

- [#6765](#6765)
[`9e7eba9`](9e7eba9)
Thanks [@kulesy](https://github.com/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.

### Patch Changes

- [#6828](#6828)
[`eafb1ff`](eafb1ff)
Thanks [@joshbermanssw](https://github.com/joshbermanssw)! - Fix
`resolveMediaCloudToRelative` so it strips any TinaCloud cloud URL on
save, not only ones whose host matches `config.assetsHost`. The match
condition is now host-agnostic: the `<clientId>/…` path prefix is the
durable invariant; the host segment can vary across stages.

This unblocks multi-host setups (PR / stage / personal-dev TinaCloud
stages) where the dashboard's default `MediaStore` inserts upload URLs
with one host while content-api returns a different one as `assetsHost`.
Previously the round-trip silently failed and absolute URLs got
committed to the content repo. After this fix, content saves as a
relative path regardless of which host the dashboard inserted, matching
pre-existing content's format.

Also covers cross-stage content migration: an absolute URL written
against one stage strips correctly when re-saved against another.

    Closes [#6827](#6827).

## @tinacms/metrics@2.1.0

### Minor Changes

- [#6738](#6738)
[`4d0c37a`](4d0c37a)
Thanks [@joshbermanssw](https://github.com/joshbermanssw)! - Stop
writing generated files (`_schema.json`, `_graphql.json`,
`_lookup.json`, `tina-lock.json`) to the content repo when
`localContentPath` is set. Generated files now live only in the
generator repo's `tina/__generated__/`. The content repo is no longer
required to contain a `tina/` folder. `FilesystemBridge.get` / `put` /
`delete` now route `tina/__generated__/` and `.tina/__generated__/`
paths to `rootPath` (the generator) instead of `outputPath` (the content
root). Closes
[tinacms/tinacloud#3295](https://github.com/tinacms/tinacloud/issues/3295).

    ### ⚠️ Rollout gate

**This release must not be promoted to the `@latest` dist-tag until
TinaCloud prod has deployed
[tinacms/tinacloud#3403](https://github.com/tinacms/tinacloud/issues/3403).**
Pre-#3403 TinaCloud reads `tina-lock.json` from the content repo on
generator pushes; shipping this change before the server-side fix breaks
every existing multi-repo user's indexing.

    ### Migration notes for existing multi-repo projects

    After upgrading (and once TinaCloud prod is on #3403):

- **Stale `tina/` folder in your content repo.** Pre-upgrade builds
committed `tina/__generated__/*` and `tina/tina-lock.json` to the
content repo. Nothing updates or reads those files any more. They are
safe — and recommended — to delete from the content repo in a single
cleanup commit.
- **`ConfigManager.generatedFolderPathContentRepo` is removed.** If any
custom CLI code, plugins, or scripts referenced this field, they will
fail at type-check or runtime. Use `generatedFolderPath` — it has always
been the generator-relative path.
- **`ConfigManager.getTinaFolderPath` no longer accepts an
`isContentRoot` option.** The content root never needs a `tina/` folder
now, so the option was removed. If any custom code called
`getTinaFolderPath(path, { isContentRoot: true })`, drop the second
argument.
- **`FilesystemBridge` behavior change for `tina/__generated__/`
paths.** In multi-repo setups, bridge reads/writes of paths under
`tina/__generated__/` or `.tina/__generated__/` now resolve against the
generator (`rootPath`) rather than the content repo (`outputPath`). If
you have custom bridge subclasses or code that relied on these paths
resolving to the content repo, update it.
- **Generated `client.ts` / `database-client.ts` now import `./types`
extensionless** (was `./types.ts`) for TypeScript projects. Avoids
requiring `allowImportingTsExtensions: true` in consumer tsconfigs,
which broke the build under Next.js 15.5+ defaults. JS projects still
import `./types.js` (Node ESM requires the extension).

## tinacms@3.8.0

### Minor Changes

- [#6771](#6771)
[`95758a0`](95758a0)
Thanks [@wicksipedia](https://github.com/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:

    ```html
    <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:

- The **`@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 markup
- An island-refresh pattern: one dynamic endpoint at
`src/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
registry
- Multi-form pages: layout fetches global, route fetches its primary
collection, both register independently — admin shows the right form
based on which marked element you click
- A **`requestWithMetadata()`** 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 visitors

    **Why 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)**

- Hugo / Eleventy adapters using the same bridge — the contract is
framework-free, just needs an integration guide
- TinaCloud overlay channel — not needed; the stateless POST protocol
works against any backend

- [#6765](#6765)
[`9e7eba9`](9e7eba9)
Thanks [@kulesy](https://github.com/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.

### Patch Changes

- [#6694](#6694)
[`723632b`](723632b)
Thanks [@alhafoudh](https://github.com/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`](95758a0)]:
    -   @tinacms/bridge@0.2.0
    -   @tinacms/search@1.2.14

## @tinacms/app@2.4.7

### Patch Changes

- Updated dependencies
\[[`723632b`](723632b),
[`95758a0`](95758a0),
[`9e7eba9`](9e7eba9)]:
    -   tinacms@3.8.0

## @tinacms/datalayer@2.0.20

### Patch Changes

- Updated dependencies
\[[`eafb1ff`](eafb1ff),
[`4d0c37a`](4d0c37a),
[`9e7eba9`](9e7eba9)]:
    -   @tinacms/graphql@2.4.0

## @tinacms/search@1.2.14

### Patch Changes

- Updated dependencies
\[[`eafb1ff`](eafb1ff),
[`4d0c37a`](4d0c37a),
[`9e7eba9`](9e7eba9)]:
    -   @tinacms/graphql@2.4.0

## @tinacms/vercel-previews@0.2.7

### Patch Changes

- Updated dependencies
\[[`723632b`](723632b),
[`95758a0`](95758a0),
[`9e7eba9`](9e7eba9)]:
    -   tinacms@3.8.0

## next-tinacms-azure@13.0.0

### Patch Changes

- Updated dependencies
\[[`723632b`](723632b),
[`95758a0`](95758a0),
[`9e7eba9`](9e7eba9)]:
    -   tinacms@3.8.0

## next-tinacms-cloudinary@25.0.0

### Patch Changes

- Updated dependencies
\[[`723632b`](723632b),
[`95758a0`](95758a0),
[`9e7eba9`](9e7eba9)]:
    -   tinacms@3.8.0

## next-tinacms-dos@22.0.0

### Patch Changes

- Updated dependencies
\[[`723632b`](723632b),
[`95758a0`](95758a0),
[`9e7eba9`](9e7eba9)]:
    -   tinacms@3.8.0

## next-tinacms-s3@22.0.0

### Patch Changes

- Updated dependencies
\[[`723632b`](723632b),
[`95758a0`](95758a0),
[`9e7eba9`](9e7eba9)]:
    -   tinacms@3.8.0

## tinacms-authjs@22.0.0

### Patch Changes

- Updated dependencies
\[[`723632b`](723632b),
[`95758a0`](95758a0),
[`9e7eba9`](9e7eba9)]:
    -   tinacms@3.8.0

## tinacms-clerk@22.0.0

### Patch Changes

- Updated dependencies
\[[`723632b`](723632b),
[`95758a0`](95758a0),
[`9e7eba9`](9e7eba9)]:
    -   tinacms@3.8.0

## tinacms-gitprovider-github@4.1.7

### Patch Changes

-   Updated dependencies \[]:
    -   @tinacms/datalayer@2.0.20

Co-authored-by: Tina Release Bot <bot@tina.io>
@pull pull Bot locked and limited conversation to collaborators May 12, 2026
@pull pull Bot added the ⤵️ pull label May 12, 2026
@pull pull Bot merged commit 8cbed24 into code:main May 12, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants