TinaCMS Astro 集成如何注册 TinaIsland 添加新的可编辑区域
【免费下载链接】tinacmsTinaCMS is the leading open-source headless CMS that supports Markdown and Visual Editing. Your content is stored in your own GitHub repo 🦙 ❤️项目地址: https://gitcode.com/GitHub_Trending/ti/tinacms
当你已经用@tinacms/astro把 TinaCMS 可视编辑接入 Astro 站点,并且至少有一个可编辑区域(island)在正常运行时,接下来常见的需求就是:再增加一个新的可编辑区域——比如新增一种内容类型的详情页,或者把页面上的某个模块单独抽出来让编辑者能点选刷新。这篇基于仓库中@tinacms/astro包的 GETTING_STARTED.md 和参考实现examples/astro/visual-editing的正文,讲清"新增一个 island"要改哪几处文件、每处怎么改、改完怎么验证。
适用环境:Astro 5/6/7、Node 18+、TinaCMS 3,项目已安装@tinacms/astro、tinacms、@tinacms/cli和一个 SSR adapter(如@astrojs/node),且astro.config.mjs里已经挂了tina()integration。
开始前确认已有基础
新增 island 的前提是项目已经走通了集成主路径,对应 GETTING_STARTED.md 的第 1–4 步:
astro.config.mjs中通过integrations: [tina()]引入集成,它会自动注入 middleware 并把 vanilla-JS bridge 作为静态文件放在/admin/bridge.js;- 存在
src/pages/tina-island/[name].ts这个动态路由,且export const prerender = false; - 数据加载函数统一用
requestWithMetadata()包裹生成的 Tina client 查询,否则编辑态下 overlay 数据不会进入渲染。
参考实现中的 data.ts 展示了标准写法:每个 collection 一个 loader,查询走tina/__generated__/client:
import { requestWithMetadata } from '@tinacms/astro'; import client from '../../tina/__generated__/client'; export const getPost = (slug: string) => requestWithMetadata(client.queries.post({ relativePath: `${slug}.md` }));如果你要新增的是全新 collection,先在tina/collections/配好 collection,再用pnpm tinacms init或重建生成客户端;如果只是复用已有 collection,直接进入下一步。
第一步:编写区域组件
为新的可编辑区域写一个独立的.astro组件,接收该区域需要的数据作为 props。参考实现中 islands/ 目录下的PostBody.astro等组件就是这种形态。以 PostBody.astro 为例,它做两件事:
- 富文本正文用
TinaMarkdown渲染(注意要从@tinacms/astro/TinaMarkdown.astro这个子路径导入,裸包的默认导出解析到的是一个占位类型,astro check会报Type 'X' is not assignable to type 'never'); - 在想要"点击即聚焦"的元素上加
data-tina-field={tinaField(data, '字段名')}标记:
--- import TinaMarkdown from '@tinacms/astro/TinaMarkdown.astro'; import { tinaField } from '@tinacms/astro/tina-field'; import type { PostQuery } from '../../../tina/__generated__/types'; interface Props { data: NonNullable<PostQuery['post']>; } const { data } = Astro.props; --- {data.title && ( <h1>import type { IslandRegistry } from '@tinacms/astro/experimental'; import AuthorBody from '../components/islands/AuthorBody.astro'; import { getAuthor } from './data'; export const islands: IslandRegistry = { // 已有条目:page、post、blog、global、global-footer … author: { fetch: (_request, params) => getAuthor(params.get('filename') ?? ''), component: AuthorBody, wrapper: SECTION_WRAPPER, propsFromData: (data) => ({ data: (data as { data?: { author?: unknown } }).data?.author, }), }, };四个字段各自的作用(依据 island-route.ts 中的注释):
fetch: (request, params) => Promise<unknown>— 解析组件所需数据,params是 URL search params,可以忽略;返回的就是 loader 的结果;component— 用取回的数据渲染的 Astro 组件;wrapper: { tag; className? }— bridge 替换进 DOM 时套用的外层元素,必须与页面侧<TinaIsland>传入的 wrapper 一致;propsFromData: (data, params) => props— 把取回的数据和 URL 参数映射为组件 props。
新增一个可编辑区域时,只需要往这个 map 里加一个条目;动态路由会自动按 URL 里的 name 去查表,不需要为每个 island 写新路由。
第三步:在页面里用<TinaIsland>包裹区域
在渲染该区域的页面中引入组件并用<TinaIsland>包住,参考实现 posts/[...urlSegments].astro 的写法:
--- import TinaIsland from '@tinacms/astro/TinaIsland.astro'; import PostBody from '../../components/islands/PostBody.astro'; import { getPost } from '../../lib/data'; import { islands } from '../../lib/islands'; const slug = (Astro.params.urlSegments ?? '').toString(); const post = await getPost(slug); if (!post.data?.post) { return new Response('Not Found', { status: 404 }); } const data = post.data.post; --- <TinaIsland name="post" wrapper={islands.post.wrapper} params={{ slug }} primary> <PostBody data={data} /> </TinaIsland><TinaIsland>的 props(见 TinaIsland.astro):
name— 与 registry 中的 key 一致,决定 bridge 去请求哪个/tina-island/[name]端点;wrapper— 直接复用islands.xxx.wrapper,保证与 registry 条目一致;params— 会序列化成 search params 传给fetch和propsFromData(例如{ slug });primary— 标记该区域是页面主表单。output: 'server'下可省略:第一次requestWithMetadata()调用(通常就是页面自己的)会被自动视为 primary;output: 'static'下必须显式指定,否则编辑器的落点可能停在 "Referenced Files" 列表上。每页最多标一个primary。
路由不用动,但要保留prerender = false
动态端点只有一个通用文件 tina-island/[name].ts,它通过experimental_createIslandRoute(islands)从注册表取对应条目渲染:
import type { APIRoute } from 'astro'; import { experimental_createIslandRoute } from '@tinacms/astro/experimental'; import { islands } from '../../lib/islands'; export const prerender = false; export const ALL: APIRoute = experimental_createIslandRoute(islands);新增 island 后这个文件不需要任何改动。唯一要注意的是保持export const prerender = false——bridge 对该端点的请求必须由 adapter 按需渲染,静态构建下它必须仍然是运行时路由。
运行与验证
启动命令(GETTING_STARTED.md 第 9、10 步):
pnpm dev # Site: http://localhost:4321/ # Admin: http://localhost:4321/admin/验证方式:打开/admin/,点进一篇文档编辑某个字段。按文档描述,成功时 iframe 只会重新拉取受影响的 island 并把它换进 DOM,整页不刷新、页面树里没有 React。针对你新增的区域,可以在 DevTools 里进一步确认:
- 打开该页面后查看
<head>,编辑模式下应包含<div contenteditable="false">【免费下载链接】tinacmsTinaCMS is the leading open-source headless CMS that supports Markdown and Visual Editing. Your content is stored in your own GitHub repo 🦙 ❤️项目地址: https://gitcode.com/GitHub_Trending/ti/tinacms
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考