[pull] main from tinacms:main#215
Merged
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 : )