关于 experience-cms-content-render
面向 React 和 Angular uiBundle 应用的交互式技能,用于渲染 CMS 内容。它安装 Salesforce CMS toolkit,并生成类型化引用、渲染器和页面放置代码,同时区分初始搭建与内容嵌入。
主要功能
- 01检测 React 和 Angular 应用
- 02处理 CMS 标识与架构
- 03注册类型化引用
- 04生成框架专用渲染器
- 05识别 Init 与 Embed 流程
适用场景
- 01向 uiBundle 应用添加 CMS 内容
- 02渲染多个同类型 CMS 项目
- 03首次使用时搭建 CMS 运行时文件
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ forcedotcom/ sf-skills --skill experience-cms-content-render安装后在 Claude Code 中输入/experience-cms-content-render确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
assets/angular/cms-content.component.tscms-item.service.tsDetailPage.component.tsMediaRenderer.component.tsTypeList.component.tsTypeRenderer.component.tsassets/react/DetailPage.tsxheuristicRenderer.tsxMediaRenderer.tsxTypeList.tsxTypeRenderer.tsxuseCmsItem.tsassets/shared/cmsContentType.tscmsCore.types.tsexternalRefs.tsmediaLabels.tsreferences/bulk-loading.mdcodegen-guardrails.mddetail-pages.mdembed-recipes.mdfailure-modes.mdheuristic-render-rules.mdinit-scaffold.mdinteraction-model.mdpackage-api.mdschema-sync.mdstyling-scopes.mdverify.md详细介绍
此技能负责指导如何将 CMS 内容渲染到 uiBundle 应用中;它不会重新实现内容交付传输。npm 包 @salesforce/ui-bundle-template-feature-cms-toolkit 负责 HTTP、信封拆包、目录键重映射、频道解析、图片 URL 解析、实体解码及其他实用功能。此技能会安装该包,然后为 React 和 Angular uiBundle 应用生成围绕该包的精简框架专用层(类型化引用、按类型划分的渲染器、获取生命周期、放置逻辑),根据文件状态推断 Init 与 Embed,并且绝不会在 Embed 提示中搭建 Init 而不告知用户。
仅含 API 的包 — 技能负责编写 UI
该工具包只导出函数和类型:不包含组件或钩子。导入目标及完整签名:references/package-api.md。三种读取方式(都接受 <TBody> 和 options?,失败时都会抛出错误):
getCmsContentByUrl— 公共 CDN,无需身份验证;粘贴的unauthenticatedUrl会按原样获取。getCmsContentByKey— 获取单个项目,通过 Connect 进行身份验证;工具包会将源键重映射为目标键并解析频道。getCmsContentByKeys— 获取多个项目,仅需一次经过身份验证的往返调用,并按键尽力处理(references/bulk-loading.md)。
此外还提供同步辅助函数 resolveCmsImageUrl、resolveMediaUrl、decodeRichHtmlEntities 以及类型化错误类。每种读取方式都会返回已解开信封的 contentBody,并将信封中的 title 提取到其中;技能代码只为 TBody 指定类型,绝不处理信封、{ items: [...] } 或 .value 包装。
随附文件(资源和参考文档会随此技能加载)
此技能的 assets/ 模板和 references/ 文档会与 SKILL.md 一同加载;请通过相对路径(assets/shared/…、assets/<framework>/…、references/…)引用,并将其内容视为可用。基础运行时文件会原样输出;只有 cmsContentType.ts 和渲染器模板包含 {{…}} 替换内容。切勿凭记忆重建模板——模板包含 StrictMode 钩子、消毒器接口以及正确的包调用位置。以下所有检测(框架、粘贴的 URL、Init/Embed/漂移)都要直接读取目标应用的文件;无需运行脚本。
交互模式(通过选项提问)
此技能是交互式的。每当提示中存在尚未决定的选项(框架、Init、漂移、内容类型、渲染目标、候选 bundle),都要通过包含 2–4 个带标签选项的选项选择问题提问,先列出推荐选项,然后等待回答——绝不能只让用户自由输入。仅在需要开放式回答时使用自由输入:组件/区域名称、URL、slug。完整措辞、选项和标题见 references/interaction-model.md;以下每处提问都必须遵循该文档。两种回答会终止流程(结束会话,不搭建任何内容):拒绝 Init,以及搜索结果为零条内容(见 HALT 行为)。
绕过模式(非交互式/自动化)。 当提示明确要求绕过交互模式(“非交互式”、“不要提问”、“全部继续/同意”、“自动继续”)时,不要提出任何选项选择问题:对于每个原本需要提问的问题,都采用其推荐选项作答并继续。这只会改变答案来源,不会改变流程逻辑,也不会覆盖终止条件——确实搜索结果为零条内容时仍须停止;存在安全默认值的情况下无法继续时,也须 HALT(未指定的原位放置、无法解析的候选 bundle)。每个问题的推荐回答见:references/interaction-model.md → 绕过模式。
框架检测(首先执行)
读取 <appRoot>/package.json(<appRoot> = 包含 package.json 的目录),合并 dependencies、devDependencies 和 peerDependencies 中的依赖名称;框架可能位于其中任一依赖类别:
- react — 存在
react,且不存在@angular/core→ React 模板。 - angular — 存在
@angular/core,且不存在react→ Angular 模板。 - ambiguous — 两者都存在 → 停止,询问要以哪个框架为目标,并提供选项(
references/interaction-model.md→ 框架):React / Angular。 - unknown — 两者都不存在,或无法读取
package.json→ 停止;这不是受支持的 React/Angular 应用;不要搭建脚手架。
框架决定以下每个步骤使用的模板系列(assets/react/* 与 assets/angular/*);assets/shared/* 与框架无关,两者都会使用。
模式
Init(一次性搭建脚手架)。 首次 Embed 时,如果框架的基础文件集(参见 Init 与 Embed 检测)不存在,则在应用根目录运行 npm i @salesforce/ui-bundle-template-feature-cms-toolkit@latest --min-release-age=0,并写入共享运行时文件一次(先询问用户)。此 npm 安装是添加工具包的唯一方式;所有导入都使用 @salesforce/… 指定符。Init 永不覆盖:完整搭建的目录树无需处理;不完整的目录树表示存在偏差,必须停止(参见 Init 与 Embed 检测)。
逐字输出运行时文件(原因见 捆绑文件):逐个读取基础模板,并原样写入 src/cms/…,跳过任何已存在的目标文件(绝不覆盖)。始终复制 assets/shared/ 中的一对文件,以及检测到的框架对应的一对文件。完整目录树、复制集表格和回退方案:references/init-scaffold.md。
Embed(逐个提示词接线)。 根据指定一个 CMS 项目(或同类型的一组项目——参见 分组)的提示词,以及提示中说明的位置,运行以下流程。当既未说明位置,也未提出详情页请求时,步骤 4 会询问位置。每个步骤都是幂等的。
Embed 流程
Identity → Schema Sync → Ref Registration → Render Target → Verify
1. 身份识别 — 解析一个 { unauthenticatedUrl | contentKey, fqn, title }
有两种 ref 类型(参见 assets/shared/cmsCore.types.ts):外部 ref——条目有 unauthenticatedUrl(公共 CDN)→ 注册一个 CmsExternalRef(url),通过 getCmsContentByUrl 读取;独立媒体除外,其 url 就是资源本身,直接使用,不进行获取(规则 1/2;在步骤 3 捕获 altText/title);uiBundle 空间 ref——条目只有 contentKey → 注册一个 CmsRef(contentKey),通过 getCmsContentByKey 读取(工具包会通过 content-metadata.json 重新映射 key)。根据输入解析身份:
- 粘贴的 URL——输入与 Experience CDN 交付 URL 匹配(正则表达式见
references/embed-recipes.md;不区分大小写,也能匹配嵌入正文中的 URL)。该 URL 即为unauthenticatedUrl;跳过搜索 → 外部 ref。不匹配 → 继续执行下方逻辑。 - 直接提供
contentKey(原始contentKey,可选channelId,无交付 URL):跳过搜索 →CmsRef。原样存储contentKey;根据步骤 3 的 channel 解析关卡,将提供的任何channelId写入public/content-metadata.json。如果CmsRef没有可解析的 channel(目录、提示词或用户均未提供),则必须停止——绝不能在没有 channel 的情况下渲染。 - 自然语言提及:将原始提示词交给 experience-search-coordinate 能力(除非提示词指定了一个组,否则单选)。它会为每个条目返回
{ unauthenticatedUrl, fqn, title, altText, contentKey };uiBundle 空间条目的unauthenticatedUrl为null,因此使用contentKey。对于媒体,将altText/title带到CmsExternalRef上(步骤 3)。
在以上三种情况中,内容类型未指定时 → 询问一次(references/interaction-model.md → 内容类型)。逐项解析与错误处理:
- 没有内容 → 停止(终止)。 搜索没有结果 → 按 HALT 行为 → 终止停止 结束本轮。绝不捏造 URL/contentKey、盲目重试或放置占位符。
- 搜索已取消/出错/不可用 → HALT(可恢复)。 请用户粘贴该条目的
unauthenticatedUrl或其contentKey(如果已知,还需提供channelId和类型);只有在用户提供身份信息后才能继续。绝不猜测。 - 既没有
unauthenticatedUrl,也没有contentKey→ HALT("'{title}' 未提供交付 URL,也没有 contentKey——无法嵌入。")。 - 否则:存在
unauthenticatedUrl→ 外部 ref;否则,有contentKey→ uiBundle 空间 ref;fqn为空 → 提出内容类型问题。
这些停止情况的问题措辞:references/interaction-model.md;完整目录:references/failure-modes.md。
组(N 个条目,类型相同)。 多个条目,或调用方传入多个 key/url,即构成一个组(内容类型相同;只询问一次类型问题)。差异如下:步骤 2 只运行一次,针对共享的 fqn(一个 <type>.ts 和一个渲染器,绝不逐项处理);步骤 3 循环处理 N 个 ref 条目,并在 ref 名称后添加短 contentKey/url 哈希,避免同名条目冲突;步骤 4 按照 组渲染目标 执行。尽力处理:跳过身份/架构验证失败的条目并报告,绝不因一个条目而 HALT 整个批次。组内同时包含 contentKey 和 unauthenticatedUrl 没有问题——每个 ref 根据自身形态进行分派。
2. 架构同步
OOTB sfdc_cms__news — 使用预定义架构,切勿获取。 元数据架构获取(ContentTypeBundle retrieve / MCP / SOQL)无法解析 OOTB Salesforce 内容类型,因此对于 fqn === 'sfdc_cms__news',完全跳过获取,并根据固定的三个字段生成 <Type>Body + <type>FieldTypes — bannerImage(Image→CmsImageField/'image')、body(RichText→string/'richText')、excerpt(Text→string/'text')— 内容逐字取自 references/schema-sync.md;不使用 sf project retrieve、MCP 或 SOQL。其他 OOTB 类型均未预定义;将它们视为自定义类型并获取。
OOTB 媒体类型 — 同样预定义,与 news 并行。 对于 fqn ∈ sfdc_cms__{image,audio,video,document},也跳过获取:该引用绑定到共享的 MediaRenderer(根据框架);不生成 <type>.ts,也不使用 heuristicRenderer。渲染器根据引用形态分支处理(外部 url → 直接使用;contentKey → 获取并按媒体类型解析;规则 2)。生成内容:references/heuristic-render-rules.md → Media types、references/init-scaffold.md。
对于自定义 fqn,通过 references/schema-sync.md 获取实时架构(使用不带前缀的 DeveloperName,去掉 sfdc_cms__/c__),将每个字段映射到 lightningType,再映射为 lowerCamel CmsFieldType 字面量(Text→'text'、RichText→'richText' 等;转换大小写,切勿直接复制 PascalCase 标记)。
- Body 模块和渲染器均已存在 → 重新获取,与当前
<Type>Body比较差异,仅重新生成<framework>/types/<type>.ts中标记下方的代码块(标记上方原样保留);完整重写渲染器。 - 任一缺失 → 根据
assets/shared/cmsContentType.ts和框架渲染器模板,在框架的types/目录中生成二者;使用者通过直接路径(../types/{{TypeRenderer}})导入各个渲染器,因此无需更新汇总文件。 - 任一标记缺失或格式错误 → 停止,要求用户恢复;切勿自动修复。获取失败 → 输出标记下方的代码块,并添加列出需手动填写字段的
TODO。
详情:references/schema-sync.md。
3. 注册 Ref
计算 EXT_<TITLE_SNAKE>(大写蛇形命名,ASCII),并将其追加到 src/cms/shared/externalRefs.ts 的 external-refs-begin 标记块中,采用与 Ref 类型匹配的形式,并在缺少相应 satisfies 类型时导入该类型。必须在放置前注册:ref('EXT_X') 的类型依据是 keyof typeof externalRefs,因此未注册的 Ref 按设计会导致编译错误。始终将 cmsType 设为 fqn DeveloperName,并使其与 satisfies 中的 <K> 匹配——省略此判别字段会导致错误类型保护静默失效。原样存储标识(绝不重新签署 URL 或手动映射 contentKey)。幂等性:名称相同且标识相同 → 跳过;名称相同但标识不同 → 停止(冲突)。两种 Ref 类型形式及其原因:references/embed-recipes.md。
外部 url Ref 上的媒体——记录 altText/title(不获取正文;规则 2):根据搜索移交的信息,在 CmsExternalRef 上设置这两个字段;对于 image/document,若两者均缺失 → 询问一次(媒体替代文本)。audio/video/contentKey Ref 跳过此步骤。
Channel 解析(仅限 CmsRef)——以目录为准。 CmsRef 必须解析出一个 Channel;按以下顺序解析,命中后即停止:(1)现有目录——public/content-metadata.json 中存在非空 channelId → 使用它,绝不覆盖;(2)提示词/标识——随 contentKey 提供了 channelId → 写入 public/content-metadata.json({ "channelId": …, "contents": [] }),如果文件不存在则创建;(3)两者皆无 → 询问用户提供 channelId,然后写入;(4)仍无法解析 → 停止(“'{title}' 是一个无法解析出 Channel 的 contentKey;渲染将失败。请提供 channelId,或将其添加到 public/content-metadata.json。”)。绝不写入占位或伪造的 Channel。**冲突:**提示词中的 Channel 与目录中的非空 Channel 不同 → 以选项选择的方式询问使用哪一个(references/interaction-model.md → Channel 冲突:保留目录值 = 不写入;使用提示词值 = 仅覆盖 channelId);只有获得确认后才能覆盖目录,绝不静默覆盖。外部 Ref 的 URL 中已包含其 Channel——无需检查。CMS_CHANNEL_ID_FALLBACK 仍仅作为谨慎使用的可选覆盖项(参见 assets/shared/externalRefs.ts)。结构、位置、优先级:references/embed-recipes.md。
4. 渲染目标 — 详情页或原位嵌入
按以下顺序确定内容的渲染位置:
- 提示词要求页面/路由 → 详情页分支,无需提问。
- 提示词指定原位放置位置(主页、侧边栏、主视觉区等) → 放置分支,无需提问。
- 两者都没有 → 提一个选项选择问题并等待(
references/interaction-model.md→ Render target):“我应该如何渲染‘{title}’?” — 专属详情页/路由 / 在现有视图中原位显示。若选择原位显示 → 接着用开放式问题询问 “‘{title}’应渲染在哪里?请指出一个组件、页面或区域。”,然后根据回答执行放置分支。
绝不猜测目标:不能默认使用详情页,也不能在未明确指定或回答目标时原位嵌入;未回答目标则必须停止。若提示词同时指定了页面和放置位置 → 询问哪个优先。
分组渲染目标(N 个项目)。 默认每个项目各用一个详情页。确定方式:(1) 提示词要求列表/网格/单页,或指定整个分组共用一个原位区域 → 列表/网格分支;(2) 提示词要求页面/路由 → 循环执行详情页分支,每个项目各生成一个页面和 slug;(3) 两者都没有 → 询问一次,默认使用详情页(references/interaction-model.md → Group render target):每个项目各用一个详情页 / 在单个页面上显示一个列表/网格。循环模式会对 slug 去重。
列表/网格分支 — 仅当目标解析选择此分支时执行。 生成框架的列表包装器(assets/<framework>/TypeList.*),每个 ref 生成一个渲染器,以确保各卡片的 fetch 相互隔离;然后通过放置分支放置一个节点,或通过详情页分支为其添加独立路由。列表与网格通过 className CSS 区分,而不是使用第二个组件(references/styling-scopes.md)。
放置分支。 在指定目标处插入对应类型的渲染器,如缺少相关导入则添加。操作具备幂等性。代码片段、配方和锚点相关的停止条件:references/embed-recipes.md。
详情页分支。 确认输入项(PageName、urlSlug、target ref、路由器文件),生成页面(基于 assets/<framework>/DetailPage.*),并将路由插入路由器的标记块中。仅追加、幂等且安全处理冲突。完整算法和前置条件停止规则:references/detail-pages.md。
样式。 此技能不会搭建 CSS 文件;展示样式以内联方式应用,并由提示词根据渲染目标指定。未指定方向时,使用默认阅读布局(居中的约 700px 列、大号字体、充足留白);原位嵌入保持简洁克制。类名钩子:references/styling-scopes.md。
5. 验证
仅对本技能写入或编辑的文件进行类型检查——范围有限,绝不循环执行。 执行一次 npx tsc --noEmit(React/Vite)或一次 npx tsc --noEmit -p tsconfig.json(Angular),然后只查看本次运行中本技能涉及文件的诊断信息;未涉及文件中的所有诊断均为已有问题,应忽略。最多进行一次修复:修复本技能生成的文件中的诊断,然后重新运行一次 tsc;绝不编辑逐字复制的基础文件(其中的诊断几乎总是工具包未解析导致的——将其作为环境说明报告,绝不修补)。不要运行 lint、build、ng build 或开发服务器;如果没有 tsc/tsconfig,则跳过并说明。然后报告写入/修改的文件、安装情况、类型检查结果、ref 身份、渲染器类型、渲染目标及任何停止条件。如果本次运行搭建了媒体,且其 ref 是 CmsRef(已认证渠道),则补充临时限制说明(内部应用目前尚不能渲染已认证渠道的媒体,后续版本将支持;使用公开 URL 的 CmsExternalRef 现在即可渲染);告知用户,但不要停止。完整修复政策和报告清单:references/verify.md。
代码生成护栏
五条规则,用于防止先前脚手架中重现的缺陷;每个生成的应用都必须满足这五条(违反任何一条都是阻塞性错误),且这些规则已写入 assets/ 模板 — 编辑时务必保持不变。下面是单行版;完整内容见 references/codegen-guardrails.md。
- 规则 1 — 调用正确的工具包读取;绝不重新实现传输。
CmsExternalRef→ 原样调用getCmsContentByUrl(url);CmsRef→ 调用getCmsContentByKey/getCmsContentByKeys。绝不重新构造 URL、手动重映射contentKey,或混用这两条路径。例外 — 不获取外部url引用中的独立媒体:其unauthenticatedUrl就是资源,直接用作元素的src/href(规则 2);新闻/结构化的CmsExternalRef仍需获取。若CmsRef需要频道,将解析出的 channelId 和空contents: []写入public/content-metadata.json(如果文件不存在则创建;若已存在频道则绝不覆盖)— 并且绝不写入占位/虚假频道(工具包在目录缺失时会开放失败,但目录存在且内容错误时不会;部署流水线会填充真实的contents)。 - 规则 2 — 先根据引用形态,再根据媒体类型解析媒体
src。 首先根据引用形态分支:外部url引用直接使用ref.url(不获取、不调用解析器,alt/title 使用引用本身的值);contentKey引用则获取正文,并通过工具包的同步解析器解析 — image →resolveCmsImageUrl(sfdc_cms:media),audio/video/document →resolveMediaUrl(sfdc_cms:media.url)(仅传主题,绝不手动添加前缀)。媒体分发依据ref.cmsType和引用形态,绝不依据正文形态。 - 规则 3 — 共享/去重的获取状态必须能在 React 19 StrictMode 下持续有效。
useCmsItem通过模块级inflight映射,在挂载→清理→重新挂载期间去重:清理时设置本地cancelled标志,绝不能中止共享请求(Angular 的CmsItemService采用相同做法)。“运行一次正常,重新挂载就失败”属于阻塞性错误。 - 规则 4 — 注入前解码 RichText;每个框架只走一条路径。 首先调用
decodeRichHtmlEntities,然后在唯一位置注入(ReactheuristicRenderer.tsx;Angularcms-content.component.ts中的[innerHTML])。绝不能同时解码和转义。 检测应匹配标签开头(<tag/<//<!)或<(RICH_TEXT_RE),绝不匹配单独的<或value.includes('<')。 - 规则 5 — 标量字段使用原始类型,绝不使用
{ value }包装。 Delivery 返回的标量是纯原始值(只有图像是对象,即CmsImageField),因此读取body.body,绝不读取body.body.value(.value解包结果始终为undefined)。根据真实负载推导形状;条目的title位于信封根部,而非contentBody中。
HALT 行为
遇到以下情况时,技能会 HALT(不写入;报告失败文件、原因和最简修复方法;等待用户):框架不明确/未知;标记缺失/格式错误;内容既没有 unauthenticatedUrl 也没有 contentKey;引用名称与不同身份冲突;渲染目标未回答/无法解析;目标路由器不明确;slug 冲突;缺少详情页前置条件;Init/基础代码发生偏移;搜索已取消/出错(可恢复 — 用户粘贴一个身份)。多条目的选择按组渲染,不会触发 HALT。完整目录:references/failure-modes.md。
终止停止 — 会话结束,不生成任何脚手架(与可恢复的 HALT 不同):(1) 拒绝 Init — 对 Run Init? 选择“No”,因此无法生成运行时脚手架,技能也无法 Embed。(2) 搜索结果为零 — experience-search-coordinate 没有找到内容,这表示该条目尚未在此 uiBundle 空间中创作;告知用户先在那里生成内容,再重新运行。两种情况都不会写入、安装或猜测。详情:references/interaction-model.md → Terminal stops。
Init 与 Embed 检测
针对目标 uiBundle 根目录(包含 src/ 的目录),检查检测到的框架的必需基础文件集中的每个路径(不是只检查单个哨兵文件),然后根据已存在文件的数量进行分类(各框架文件列表和 bundle 解析规则:references/init-scaffold.md):
- embed — 全部存在 → 运行 Embed 流程。
- init — 全部缺失 → 提供选项并询问(
references/interaction-model.md→ Run Init?):是,进行脚手架搭建 / 否。选择是 → 运行 Init。选择否 → 终止(参见 HALT behaviour → Terminal stops):说明用户拒绝了 Init,不写入或安装任何内容,并结束本轮;不得转而运行 Embed 或只搭建部分脚手架。 - drift — 部分存在、部分缺失 → 停止(运行时不完整时绝不运行 Embed)。逐字报告已存在与缺失的列表,然后询问(
references/interaction-model.md→ Complete a partial runtime?):是,写入缺失文件 / 否。选择是 → 逐字写入仅缺失的文件(读取→写入,跳过已存在文件;references/init-scaffold.md),然后重新检查;绝不触碰或“刷新”已存在的文件。选择否 → 停止。绝不默默补齐、覆盖或重置已存在的文件。
如果提示词说明运行时已经搭建好脚手架,这是一个强烈的 Embed 信号;仍须执行检查,但不要运行 Init。
Embed 仅处理增量——绝不重新输出运行时。 检测到 Embed 后,本次运行最多只写入新的 <framework>/types/<type>.ts + renderer,以及对 shared/externalRefs.ts 和渲染目标中的标记块进行编辑。每个已存在的基础文件都必须保持逐字节不变——不要重新搭建脚手架、重新复制、“刷新”或再次运行 npm install;重新输出 Init 模板(例如将 externalRefs 重置为 {})是阻断性错误。基础文件看起来有问题 → 作为 Init 漂移停止,绝不默默覆盖。
批量读取
默认按项读取:每个 renderer 独立读取,隔离每张卡片的失败情况。若要通过一次经过身份验证的往返读取一组已知的 contentKey,请直接调用 getCmsContentByKeys;无需搭建批量加载器。尽力返回每个键的结果;仅 contentKey 支持批量读取(外部 URL 单独读取)。references/bulk-loading.md。
不在范围内
动态发现 feed(“最新 10 条新闻”);跨引用审计工具;跨组织的媒体/图片引用映射;React + Angular uiBundles 以外的框架。
文件与参考资料
模板 — assets/shared/:cmsCore.types.ts、externalRefs.ts、cmsContentType.ts(基础文件 + 各类型的正文)、mediaLabels.ts。assets/react/:useCmsItem.ts、heuristicRenderer.tsx(renderer + RenderField 分发器)、TypeRenderer.tsx、TypeList.tsx、DetailPage.tsx、MediaRenderer.tsx。assets/angular/:cms-item.service.ts、cms-content.component.ts、TypeRenderer.component.ts、TypeList.component.ts、DetailPage.component.ts、MediaRenderer.component.ts。
文档(references/):package-api.md(工具包 API + 契约——事实依据)、interaction-model.md(措辞 + 终止条件)、init-scaffold.md(运行时目录树、复制集合、基础文件集)、schema-sync.md(检索 + 重新生成)、verify.md(修复策略 + 报告)、codegen-guardrails.md(完整规则 1–5)、embed-recipes.md(URL 正则 + 放置位置)、heuristic-render-rules.md、detail-pages.md、styling-scopes.md、bulk-loading.md、failure-modes.md。