experience-cms-content-render

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese
This skill directs how to render CMS content into a uiBundle app; it does NOT re-implement the delivery transport. The npm package
@salesforce/ui-bundle-template-feature-cms-toolkit
owns HTTP, envelope-unwrap, catalog key-remap, channel resolution, image-URL resolution, entity decoding and other utilities. The skill installs that package, then generates the thin framework-specific layer around it (typed refs, per-type renderers, fetch lifecycle, placement) for React and Angular uiBundle apps, inferring Init vs Embed from file state and never scaffolding Init inside an Embed prompt without telling the user.
本技能指导如何将CMS内容渲染到uiBundle应用中;不重新实现交付传输逻辑。npm包**
@salesforce/ui-bundle-template-feature-cms-toolkit
**负责HTTP请求、信封解析、目录键重映射、渠道解析、图片URL解析、实体解码等工具类功能。该技能会安装此包,然后为React和Angular uiBundle应用生成基于该包的轻量框架特定层(类型化引用、按类型划分的渲染器、获取生命周期、放置逻辑),根据文件状态推断是初始化(Init)还是嵌入(Embed)模式,且不会在未告知用户的情况下在Embed提示中搭建Init脚手架。

API-only package — the skill writes the UI

仅API包——由该技能编写UI

The toolkit exports functions and types only: no components, no hooks. Import target and full signatures:
references/package-api.md
. The three reads (all take
<TBody>
+
options?
, all throw on failure):
  • getCmsContentByUrl
    — public CDN, unauthenticated; the pasted
    unauthenticatedUrl
    fetched AS-IS.
  • getCmsContentByKey
    — one item, authenticated Connect; the toolkit remaps source→target key and resolves the channel.
  • getCmsContentByKeys
    — many, ONE authenticated round-trip, best-effort per-key (
    references/bulk-loading.md
    ).
Plus sync helpers
resolveCmsImageUrl
,
resolveMediaUrl
,
decodeRichHtmlEntities
and the typed error classes. Every read returns the already-UNWRAPPED
contentBody
with the envelope
title
lifted in; skill code only types
TBody
and never handles the envelope,
{ items: [...] }
, or a
.value
wrapper.
工具包仅导出函数和类型:无组件、无钩子。导入目标及完整签名请参考:
references/package-api.md
。三种读取方法(均接受
<TBody>
+
options?
参数,失败时均会抛出异常):
  • getCmsContentByUrl
    —— 公共CDN,无需认证;直接获取传入的
    unauthenticatedUrl
    。
  • getCmsContentByKey
    —— 获取单个条目,需认证Connect;工具包会重映射源→目标键并解析渠道。
  • getCmsContentByKeys
    —— 获取多个条目,单次认证请求,按键做最大努力尝试(详见
    references/bulk-loading.md
    )。
此外还有同步辅助函数
resolveCmsImageUrl
、
resolveMediaUrl
、
decodeRichHtmlEntities
以及类型化错误类。所有读取方法均返回已解析的
contentBody
,并提取信封中的
title
;技能代码仅需为
TBody
指定类型,无需处理信封、
{ items: [...] }
或
.value
包装器。

Bundled files (assets & references load with this skill)

捆绑文件(资源和参考文档随技能加载)

This skill's
assets/
templates and
references/
docs load alongside SKILL.md; reference them by relative path (
assets/shared/…
,
assets/<framework>/…
,
references/…
) and treat their contents as available. Foundation runtime files are emitted VERBATIM; only
cmsContentType.ts
and the renderer templates carry
{{…}}
substitution. Never reconstruct a template from memory — the templates encode the StrictMode hook, the sanitizer seam, and the correct package call sites. Every detection below (framework, pasted-URL, Init/Embed/drift) READS the target app's files directly; there is no script to run.
本技能的
assets/
模板和
references/
文档会与SKILL.md一同加载;可通过相对路径引用(如
assets/shared/…
、
assets/<framework>/…
、
references/…
),并可直接使用其内容。基础运行时文件会原封不动生成;仅
cmsContentType.ts
和渲染器模板包含
{{…}}
替换逻辑。请勿凭记忆重构模板——模板中包含StrictMode钩子、清理器接口以及正确的包调用位置。以下所有检测(框架、粘贴的URL、Init/Embed/漂移状态)均直接读取目标应用的文件;无需运行脚本。

Interaction model (ask with options)

交互模型(带选项询问)

This skill is interactive. At every decision the prompt leaves open (framework, Init, drift, content type, render target, candidate bundle) ASK with an option-select question of 2–4 labeled options, recommended first, and wait — never a bare free-text prompt. Use free-text only for open answers: a component/region name, a URL, a slug. Full wording, options, and headers:
references/interaction-model.md
; follow it for every ASK below. Two answers are terminal (end the session, scaffold nothing): declining Init, and a search that returns zero content (see HALT behaviour).
Bypass mode (non-interactive / automation). When the prompt explicitly asks to bypass the interaction model ("non-interactive", "don't ask", "proceed/yes to all", "auto-proceed"), ask NO option-select question: answer every would-be question with its recommended option and continue. This changes only the answer source, not pipeline logic, and does NOT override the terminal stops — a genuine zero-content result still stops, as does any true HALT with no safe default (unnamed in-place placement, unresolvable candidate bundle). Per-question recommended answers:
references/interaction-model.md
→ Bypass mode.
本技能为交互式。对于提示中未明确的每个决策点(框架、Init、漂移、内容类型、渲染目标、候选包),需以选项选择的形式提出2-4个带标签的选项(推荐选项放在首位)并等待用户回复——绝不使用无约束的自由文本提示。仅在需要开放式答案时使用自由文本:如组件/区域名称、URL、slug。完整措辞、选项和标题请参考
references/interaction-model.md
;以下所有询问均需遵循该文档。两种答案为终止性回复(结束会话,不搭建任何内容):拒绝Init,以及搜索返回零内容(详见HALT行为)。
绕过模式(非交互式/自动化)。当提示明确要求绕过交互模型(如"非交互式"、"不要询问"、"全部确认/同意"、"自动执行")时,不提出选项选择问题:所有本应询问的问题均使用推荐选项作答并继续。此模式仅改变答案来源,不改变流程逻辑,且不覆盖终止性停止条件——真实的零内容结果仍会终止会话,任何无安全默认值的真正HALT(如未命名的就地放置、无法解析的候选包)也会终止会话。各问题的推荐答案请参考
references/interaction-model.md
→绕过模式。

Framework detection (do this FIRST)

框架检测(优先执行)

Read
<appRoot>/package.json
(
<appRoot>
= the dir with
package.json
) and merge dependency names from
dependencies
,
devDependencies
, and
peerDependencies
; a framework may sit in any bucket:
  • react —
    react
    present,
    @angular/core
    absent → React templates.
  • angular —
    @angular/core
    present,
    react
    absent → Angular templates.
  • ambiguous — BOTH present → HALT, ask which framework to target with options (
    references/interaction-model.md
    → Framework): React / Angular.
  • unknown — neither present, or no readable
    package.json
    → HALT, not a supported React/Angular app; do not scaffold.
The framework selects the template family for every step below (
assets/react/*
vs
assets/angular/*
);
assets/shared/*
is framework-agnostic and used by both.
读取
<appRoot>/package.json
(
<appRoot>
为包含
package.json
的目录),合并
dependencies
、
devDependencies
和
peerDependencies
中的依赖名称;框架可能存在于任意依赖项中:
  • react —— 存在
    react
    依赖,不存在
    @angular/core
    依赖 → 使用React模板。
  • angular —— 存在
    @angular/core
    依赖,不存在
    react
    依赖 → 使用Angular模板。
  • 模糊 —— 两者均存在 → HALT,询问用户要针对哪个框架(参考
    references/interaction-model.md
    →框架选项:React / Angular)。
  • 未知 —— 两者均不存在,或无法读取
    package.json
    → HALT,不支持该非React/Angular应用;不搭建脚手架。
框架选择决定后续每一步使用的模板系列(
assets/react/*
vs
assets/angular/*
);
assets/shared/*
为框架无关模板,两者均会使用。

Modes

模式

Init (one-time scaffold). On the first Embed, when the framework's foundation set (see Init vs Embed detection) is absent, run
npm i @salesforce/ui-bundle-template-feature-cms-toolkit@latest --min-release-age=0
in the app root and write the shared runtime once (prompt the user first). This npm install is the ONLY way the toolkit is added; every import uses the
@salesforce/…
specifier. Init never overwrites: a fully-scaffolded tree is a no-op; a partial tree is drift and HALTS (see Init vs Embed detection).
Emit runtime files VERBATIM (rationale under Bundled files):
Read
each foundation template and
Write
it unchanged to
src/cms/…
, skipping any destination that already exists (never overwrite). Copy the
assets/shared/
pair always, plus the detected framework's pair. Full tree, copy-set table, and fallback:
references/init-scaffold.md
.
Embed (per-prompt wiring). Given a prompt naming one CMS item (or a same-type group — see Groups), plus a placement when stated, run the pipeline below. When neither a placement nor a detail-page request is stated, step 4 asks which. Every step is idempotent.
初始化(Init,一次性脚手架搭建)。首次执行Embed时,若框架的基础集(详见Init vs Embed检测)不存在,则在应用根目录运行
npm i @salesforce/ui-bundle-template-feature-cms-toolkit@latest --min-release-age=0
,并一次性写入共享运行时(需先提示用户)。此npm安装是添加工具包的唯一方式;所有导入均使用
@salesforce/…
标识符。Init不会覆盖内容:若已完全搭建好目录树则无操作;若目录树不完整(漂移状态)则HALT(详见Init vs Embed检测)。
原封不动生成运行时文件(原因见捆绑文件):读取每个基础模板并原样写入
src/cms/…
,跳过已存在的目标文件(绝不覆盖)。始终复制
assets/shared/
下的文件,再加上检测到的框架对应的文件。完整目录树、复制集表及回退方案请参考
references/init-scaffold.md
。
嵌入(Embed,按提示配置)。给定一个指定单个CMS条目(或同类型组——详见组)的提示,以及指定的放置位置(若有),执行以下流程。若未指定放置位置或详情页请求,则步骤4会询问目标位置。每一步均为幂等操作。

Embed pipeline

嵌入流程

text
Identity → Schema Sync → Ref Registration → Render Target → Verify
text
身份解析 → Schema同步 → 引用注册 → 渲染目标 → 验证

1. Identity — resolve one
{ unauthenticatedUrl | contentKey, fqn, title }

1. 身份解析 —— 解析单个
{ unauthenticatedUrl | contentKey, fqn, title }

Two ref types (see
assets/shared/cmsCore.types.ts
): a foreign ref — item has an
unauthenticatedUrl
(public CDN) → register a
CmsExternalRef
(
url
), read via
getCmsContentByUrl
— except standalone media, whose url is the asset, used directly, not fetched (Rule 1/2; capture
altText
/
title
at step 3); a uiBundle-space ref — item has only a
contentKey
→ register a
CmsRef
(
contentKey
), read via
getCmsContentByKey
(toolkit remaps the key via
content-metadata.json
). Resolve the identity from the input:
  • URL pasted — the input matches an Experience CDN delivery URL (regex in
    references/embed-recipes.md
    ; case-insensitive, matches a URL embedded in prose). That URL IS the
    unauthenticatedUrl
    ; skip search → foreign ref. No match → fall through below.
  • contentKey given directly (raw
    contentKey
    , optional
    channelId
    , no delivery URL): skip search →
    CmsRef
    . Store the
    contentKey
    verbatim; route any given
    channelId
    into
    public/content-metadata.json
    per the step-3 channel-resolution gate. A
    CmsRef
    with no resolvable channel (none in the catalog, prompt, or from the user) HALTs — never render without one.
  • Natural-language mention: delegate to the experience-search-coordinate capability with the raw prompt (single-select unless the prompt names a group). It returns
    { unauthenticatedUrl, fqn, title, altText, contentKey }
    per item;
    unauthenticatedUrl
    is
    null
    for uiBundle-space items, so use
    contentKey
    . For media, carry
    altText
    /
    title
    onto the
    CmsExternalRef
    (step 3).
In all three, an unnamed content type → ask once (
references/interaction-model.md
→ Content type). Per-item resolution and error handling:
  • Zero content → STOP (terminal). Search finds nothing → end the turn per HALT behaviour → Terminal stops. Never fabricate a URL/contentKey, retry blindly, or place a placeholder.
  • Search cancelled/errored/unavailable → HALT (recoverable). Ask the user to paste the item's
    unauthenticatedUrl
    OR its
    contentKey
    (+
    channelId
    and type if known); resume only once they provide an identity. Never guess.
  • Neither
    unauthenticatedUrl
    nor
    contentKey
    → HALT ("'{title}' exposes no delivery URL and no contentKey — cannot embed.").
  • Otherwise:
    unauthenticatedUrl
    present → foreign ref; else
    contentKey
    → uiBundle-space ref;
    fqn
    empty → ask the content-type question.
Question wording for these stops:
references/interaction-model.md
; full catalogue:
references/failure-modes.md
.
Groups (N items, same type). More than one item, or a caller passing multiple keys/urls, is a group (one content type; ask the type question ONCE). Deltas: step 2 runs ONCE for the shared
fqn
(one
<type>.ts
+ one renderer, never per item); step 3 loops N ref entries, suffixing the ref name with a short contentKey/url hash so same-title items don't collide; step 4 follows Group render target. Best-effort: skip and report an item that fails identity/schema, never HALT the whole batch on one. A group mixing
contentKey
and
unauthenticatedUrl
is fine — each ref dispatches by its own shape.
两种引用类型(详见
assets/shared/cmsCore.types.ts
):外部引用——条目包含
unauthenticatedUrl
(公共CDN)→ 注册
CmsExternalRef
(
url
),通过
getCmsContentByUrl
读取——独立媒体除外,其URL即为资产,直接使用,无需获取(规则1/2;在步骤3捕获
altText
/
title
);uiBundle空间引用——条目仅包含
contentKey
→ 注册
CmsRef
(
contentKey
),通过
getCmsContentByKey
读取(工具包通过
content-metadata.json
重映射键)。从输入中解析身份:
  • 粘贴URL——输入匹配Experience CDN交付URL(正则表达式见
    references/embed-recipes.md
    ;不区分大小写,匹配嵌入在文本中的URL)。该URL即为
    unauthenticatedUrl
    ;跳过搜索→外部引用。不匹配则进入以下流程。
  • 直接给定contentKey(原始
    contentKey
    ,可选
    channelId
    ,无交付URL):跳过搜索→
    CmsRef
    。原样存储
    contentKey
    ;将给定的
    channelId
    按步骤3的渠道解析规则存入
    public/content-metadata.json
    。若
    CmsRef
    无法解析渠道(目录、提示或用户均未提供)→ HALT——绝不无渠道渲染。
  • 自然语言提及:将原始提示委托给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
    →外部引用;否则使用
    contentKey
    →uiBundle空间引用;
    fqn
    为空→询问内容类型问题。
这些停止场景的提问措辞请参考
references/interaction-model.md
;完整列表请参考
references/failure-modes.md
。
组(N个条目,同类型)。多个条目,或调用者传入多个键/URL,即为组(单一内容类型;仅询问一次类型问题)。差异点:步骤2仅执行一次(针对共享
fqn
生成一个
<type>.ts
+ 一个渲染器,绝不逐条执行);步骤3循环处理N个引用条目,在引用名称后添加短contentKey/URL哈希,避免同标题条目冲突;步骤4遵循组渲染目标规则。最大努力尝试:跳过并报告身份/schema解析失败的条目,绝不因单个条目失败而终止整个批次。组中混合
contentKey
和
unauthenticatedUrl
是允许的——每个引用按自身类型分发。

2. Schema Sync

2. Schema同步

OOTB
sfdc_cms__news
— use the predefined schema, do NOT fetch.
The metadata schema fetch (ContentTypeBundle retrieve / MCP / SOQL) cannot resolve OOTB Salesforce content types, so for
fqn === 'sfdc_cms__news'
skip retrieval entirely and generate
<Type>Body
+
<type>FieldTypes
from the fixed three-field set —
bannerImage
(Image→
CmsImageField
/
'image'
),
body
(RichText→
string
/
'richText'
),
excerpt
(Text→
string
/
'text'
) — verbatim from
references/schema-sync.md
; no
sf project retrieve
, MCP, or SOQL. Other OOTB types are not predefined; treat them as custom and fetch.
OOTB media — predefined too, parallel to news. For
fqn ∈ sfdc_cms__{image,audio,video,document}
also skip retrieval: the ref binds to the shared verbatim
MediaRenderer
(per framework) — no generated
<type>.ts
, no
heuristicRenderer
. The renderer forks by ref shape (foreign
url
→ used directly;
contentKey
→ fetch + resolve by medium; Rule 2). Emission:
references/heuristic-render-rules.md
→ Media types,
references/init-scaffold.md
.
For a custom
fqn
, fetch the live schema (bare DeveloperName, strip
sfdc_cms__
/
c__
) via
references/schema-sync.md
, mapping each field to a
lightningType
, then to the lowerCamel
CmsFieldType
literal
(
Text
→
'text'
,
RichText
→
'richText'
, …; translate the casing, never copy the PascalCase token).
  • Body module + renderer both exist → re-fetch, diff against the current
    <Type>Body
    , regenerate ONLY the below-marker block of
    <framework>/types/<type>.ts
    (above-marker preserved verbatim); rewrite the renderer in full.
  • Either missing → generate both from
    assets/shared/cmsContentType.ts
    and the framework renderer template into the framework's
    types/
    dir; consumers import each renderer by direct path (
    ../types/{{TypeRenderer}}
    ), so there is no barrel to update.
  • Any marker missing/malformed → HALT, ask the user to restore; never auto-heal. Retrieval fails → emit the below-marker block with a
    TODO
    listing fields to fill manually.
Details:
references/schema-sync.md
.
原生
fqn === 'sfdc_cms__news'
——使用预定义schema,无需获取
。元数据schema获取(ContentTypeBundle检索/MCP/SOQL)无法解析Salesforce原生内容类型,因此当
fqn === 'sfdc_cms__news'
时,完全跳过检索,从固定的三个字段集生成
<Type>Body
+
<type>FieldTypes
——
bannerImage
(图片→
CmsImageField
/
'image'
)、
body
(富文本→
string
/
'richText'
)、
excerpt
(文本→
string
/
'text'
)——完全参考
references/schema-sync.md
;无需执行
sf project retrieve
、MCP或SOQL。其他原生类型无预定义;视为自定义类型并获取。
原生媒体——同样预定义,与新闻类型并行。当
fqn ∈ sfdc_cms__{image,audio,video,document}
时也跳过检索:引用绑定到共享的原生
MediaRenderer
(按框架区分)——不生成
<type>.ts
,不使用
heuristicRenderer
。渲染器按引用类型分支(外部
url
→直接使用;
contentKey
→获取+按媒体类型解析;规则2)。生成规则参考
references/heuristic-render-rules.md
→媒体类型、
references/init-scaffold.md
。
对于自定义
fqn
,通过
references/schema-sync.md
获取实时schema(仅保留DeveloperName,去除
sfdc_cms__
/
c__
前缀),将每个字段映射到
lightningType
,再转换为小驼峰式
CmsFieldType
字面量
(
Text
→
'text'
,
RichText
→
'richText'
,…;转换大小写,绝不复制大驼峰式标记)。
  • Body模块和渲染器均已存在→重新获取,与当前
    <Type>Body
    对比差异,仅重新生成
    <framework>/types/<type>.ts
    中标记下方的代码块(标记上方的内容原样保留);完全重写渲染器。
  • 任一缺失→从
    assets/shared/cmsContentType.ts
    和框架渲染器模板生成两者,存入框架的
    types/
    目录;消费者通过直接路径导入每个渲染器(
    ../types/{{TypeRenderer}}
    ),因此无需更新桶文件。
  • 任何标记缺失/格式错误→HALT,询问用户恢复;绝不自动修复。检索失败→在标记下方的代码块中生成包含待手动填充字段的
    TODO
    注释。
详细信息参考
references/schema-sync.md
。

3. Ref Registration

3. 引用注册

Compute
EXT_<TITLE_SNAKE>
(upper-snake, ASCII) and append inside the
external-refs-begin
marker block of
src/cms/shared/externalRefs.ts
, emitting the form matching the ref type and importing its
satisfies
type if absent. Register BEFORE placement:
ref('EXT_X')
is typed against
keyof typeof externalRefs
, so an unregistered ref is a compile error by design. Always set
cmsType
to the fqn DeveloperName and match the
<K>
in
satisfies
— omit this discriminator and the wrong-type guard silently disappears. Store the identity verbatim (never re-sign a URL or hand-map a
contentKey
). Idempotency: same name + same identity → skip; same name + different identity → HALT (collision). Both ref-type forms + rationale:
references/embed-recipes.md
.
Media on a foreign
url
ref — capture
altText
/
title
(no fetched body; Rule 2): set them on the
CmsExternalRef
from the search hand-off; both absent for
image
/
document
→ ASK once (Media alt text).
audio
/
video
/
contentKey
refs skip it.
Channel resolution (CmsRef only) — catalog-primary. A
CmsRef
MUST resolve a channel; resolve in order, stop at the first hit: (1) existing catalog — a non-empty
channelId
in
public/content-metadata.json
→ use it, never overwrite; (2) prompt/identity — a
channelId
came with the
contentKey
→ write
public/content-metadata.json
(
{ "channelId": …, "contents": [] }
), creating it if absent; (3) neither → ASK the user for the channelId, then write it; (4) still unresolved → HALT ("'{title}' is a contentKey with no resolvable channel; rendering will fail. Provide the channelId or add it to public/content-metadata.json."). Never write a placeholder/fake channel. Conflict: a prompt channel that differs from a non-empty catalog channel → ASK which wins as an option-select (
references/interaction-model.md
→ Channel conflict: keep catalog = no write; use prompt = overwrite only
channelId
); overwrite the catalog only on confirmation, never silently. Foreign refs carry their channel in the URL — no gate.
CMS_CHANNEL_ID_FALLBACK
remains only as a cautioned, opt-in override (see
assets/shared/externalRefs.ts
). Shape, location, precedence:
references/embed-recipes.md
.
计算
EXT_<TITLE_SNAKE>
(大写蛇形命名,仅ASCII字符),并将其添加到
src/cms/shared/externalRefs.ts
的
external-refs-begin
标记块内,生成与引用类型匹配的格式,若相关
satisfies
类型未导入则添加导入。注册需在放置前完成:
ref('EXT_X')
的类型依赖于
keyof typeof externalRefs
,因此未注册的引用会触发编译错误。始终将
cmsType
设置为fqn的DeveloperName,并匹配
satisfies
中的
<K>
——省略该鉴别器会导致类型守卫失效。原样存储身份信息(绝不重新签名URL或手动映射
contentKey
)。幂等性:相同名称+相同身份→跳过;相同名称+不同身份→HALT(冲突)。两种引用类型的格式及原理参考
references/embed-recipes.md
。
外部
url
引用的媒体——捕获
altText
/
title
(无获取的body;规则2):从搜索结果中获取并设置到
CmsExternalRef
;若
image
/
document
的两者均缺失→询问一次(媒体替代文本)。
audio
/
video
/
contentKey
引用跳过此步骤。
渠道解析(仅CmsRef)——目录优先。
CmsRef
必须解析渠道;按以下顺序解析,找到第一个匹配项即停止:(1) 现有目录——
public/content-metadata.json
中存在非空
channelId
→使用该值,绝不覆盖;(2) 提示/身份信息——
contentKey
附带
channelId
→写入
public/content-metadata.json
(
{ "channelId": …, "contents": [] }
),不存在则创建;(3) 均无→询问用户提供channelId,然后写入;(4) 仍无法解析→HALT("'{title}'是无可用解析渠道的contentKey;渲染会失败。请提供channelId或添加到public/content-metadata.json中。")。绝不写入占位符/虚假渠道。冲突:提示中的渠道与目录中的非空渠道不同→以选项选择形式询问用户优先级(参考
references/interaction-model.md
→渠道冲突:保留目录=不写入;使用提示=仅覆盖
channelId
);仅在用户确认后覆盖目录,绝不静默覆盖。外部引用的渠道包含在URL中——无需此步骤。
CMS_CHANNEL_ID_FALLBACK
仅作为谨慎的可选覆盖项(详见
assets/shared/externalRefs.ts
)。格式、位置、优先级参考
references/embed-recipes.md
。

4. Render Target — detail page or in-place embed

4. 渲染目标——详情页或就地嵌入

Resolve WHERE the content renders, in order:
  1. Prompt asks for a page/route → Detail Page branch, no question.
  2. Prompt names an in-place placement (home page, sidebar, hero, …) → Placement branch, no question.
  3. Neither → ASK ONE option-select question and wait (
    references/interaction-model.md
    → Render target): "How should I render '{title}'?" — Dedicated detail page/route / In place in an existing view. In-place → follow up with an open answer "Where should '{title}' render? Name a component, page, or region.", then run Placement against the answer.
Never guess a target: no detail page implicitly, no in-place embed without a stated/answered target; an unanswered target HALTs. A prompt naming both a page and a placement → ask which takes precedence.
Group render target (N items). Default is a detail page PER item. Resolve: (1) prompt asks for list/grid/one page, or names ONE in-place region for the whole group → List/Grid branch; (2) prompt asks for pages/routes → loop Detail Page branch, one page + slug per item; (3) neither → ASK once, default detail pages (
references/interaction-model.md
→ Group render target): A detail page for each item / One list/grid on a single page. Loop mode dedupes slugs.
List/Grid branch — only when target resolution selected it. Generate the framework's list wrapper (
assets/<framework>/TypeList.*
), one renderer per ref so per-card fetch isolation applies, then place ONE node via the Placement branch or its own route via the Detail Page branch. List vs grid is
className
CSS, not a second component (
references/styling-scopes.md
).
Placement branch. Insert the per-type renderer at the named target, adding imports if missing. Idempotent. Snippets, recipes, and anchor HALTs:
references/embed-recipes.md
.
Detail Page branch. Confirm inputs (PageName, urlSlug, target ref, router file), generate the page (from
assets/<framework>/DetailPage.*
), and insert the route in the router's marker block. Append-only, idempotent, collision-safe. Full algorithm + prerequisite HALTs:
references/detail-pages.md
.
Styling. The skill scaffolds NO CSS file; presentation is inline and prompt-driven on the render target. No direction → a default reading layout (centered ~700px column, large type, ample whitespace); in-place embeds stay conservative. Class hooks:
references/styling-scopes.md
.
按以下顺序解析内容渲染位置:
  1. 提示要求页面/路由→详情页分支,无需询问。
  2. 提示指定就地放置位置(首页、侧边栏、Hero区域等)→放置分支,无需询问。
  3. 均未指定→提出一个选项选择问题并等待回复(参考
    references/interaction-model.md
    →渲染目标):"应如何渲染'{title}'?"——专属详情页/路由 / 在现有视图中就地嵌入。选择就地嵌入→跟进开放式问题"'{title}'应渲染到哪里?请指定组件、页面或区域名称。",然后针对答案执行放置分支。
绝不猜测目标位置:不默认使用详情页,无指定/回复的目标位置时不执行就地嵌入;未回复目标位置则HALT。若提示同时指定页面和放置位置→询问用户优先级。
组渲染目标(N个条目)。默认每个条目对应一个详情页。解析规则:(1) 提示要求列表/网格/单个页面,或为整个组指定一个就地区域→列表/网格分支;(2) 提示要求页面/路由→循环执行详情页分支,每个条目对应一个页面+slug;(3) 均未指定→询问一次,默认详情页(参考
references/interaction-model.md
→组渲染目标):每个条目对应一个详情页 / 在单个页面上生成列表/网格。循环模式会去重slug。
列表/网格分支——仅在目标解析选择该模式时执行。生成框架的列表包装器(
assets/<framework>/TypeList.*
),每个引用对应一个渲染器(实现每张卡片独立获取,隔离失败),然后通过放置分支放置一个节点,或通过详情页分支配置独立路由。列表与网格的区别仅在于
className
CSS,无需单独组件(参考
references/styling-scopes.md
)。
放置分支。在指定目标位置插入对应类型的渲染器,若缺失则添加导入。幂等操作。代码片段、示例及锚点HALT规则参考
references/embed-recipes.md
。
详情页分支。确认输入(PageName、urlSlug、目标引用、路由文件),生成页面(来自
assets/<framework>/DetailPage.*
),并在路由的标记块中插入路由。仅追加内容,幂等操作,避免冲突。完整算法及前置HALT规则参考
references/detail-pages.md
。
样式。该技能不搭建CSS文件;展示样式为内联样式,由渲染目标的提示驱动。无样式要求→默认阅读布局(居中约700px列、大字体、充足留白);就地嵌入保持保守样式。类钩子参考
references/styling-scopes.md
。

5. Verify

5. 验证

Typecheck ONLY the files this skill wrote or edited — bounded, never a loop. Run
npx tsc --noEmit
ONCE (React/Vite) or
npx tsc --noEmit -p tsconfig.json
ONCE (Angular), then read ONLY the diagnostics in files the skill touched this run; every diagnostic in an untouched file is pre-existing — ignore it. At most ONE fix pass: fix a diagnostic in a file the skill generated and re-run tsc once; never edit a verbatim foundation file (a diagnostic there is almost always the toolkit unresolved — report it as an environment note, never patch). Do NOT run
lint
,
build
,
ng build
, or the dev server; no
tsc
/
tsconfig
→ skip and note it. Then report files written/modified, install, typecheck result, ref identity, renderer type, render target, and any HALTs. If this run scaffolded media whose ref is a
CmsRef
(authenticated channel), add the temporary-limitation note (internal apps can't render authenticated-channel media yet — a future release; a public-URL
CmsExternalRef
renders today); inform, don't HALT. Full fix policy + report checklist:
references/verify.md
.
仅对该技能编写或编辑的文件进行类型检查——范围明确,绝不循环执行。运行
npx tsc --noEmit
一次(React/Vite)或
npx tsc --noEmit -p tsconfig.json
一次(Angular),然后仅读取本次运行中该技能修改的文件的诊断信息;未修改文件中的所有诊断信息均为预先存在的——忽略。最多执行一次修复:修复该技能生成的文件中的诊断信息,然后重新运行一次tsc;绝不修改原生基础文件(该文件中的诊断信息几乎总是工具包未解析导致——作为环境说明报告,绝不修补)。不运行
lint
、
build
、
ng build
或开发服务器;无
tsc
/
tsconfig
→跳过并说明。然后报告编写/修改的文件、安装情况、类型检查结果、引用身份、渲染器类型、渲染目标及所有HALT情况。若本次运行搭建的媒体引用为
CmsRef
(认证渠道),需添加临时限制说明(内部应用目前无法渲染认证渠道的媒体——后续版本将支持;公共URL的
CmsExternalRef
当前即可渲染);仅告知用户,不HALT。完整修复策略及报告清单参考
references/verify.md
。

Codegen guardrails

代码生成约束

Five rules encoding defects a prior scaffold reproduced; every generated app MUST satisfy all five (a violation is a blocking bug), and they are baked into the
assets/
templates — keep them true when editing. One-line form below; full text in
references/codegen-guardrails.md
.
  • Rule 1 — Call the right toolkit read; never re-implement transport.
    CmsExternalRef
    →
    getCmsContentByUrl(url)
    AS-IS;
    CmsRef
    →
    getCmsContentByKey
    /
    getCmsContentByKeys
    . Never reconstruct a URL, hand-remap a
    contentKey
    , or cross the paths. Exception — standalone media on a foreign
    url
    ref is NOT fetched:
    its
    unauthenticatedUrl
    is the asset, used directly as the element
    src
    /
    href
    (Rule 2); news/structured
    CmsExternalRef
    s still fetch. When a
    CmsRef
    needs a channel, write
    public/content-metadata.json
    with the resolved channelId and empty
    contents: []
    (create if absent; never overwrite a present channel) — and never write a placeholder/fake channel (the toolkit fails open on a missing catalog, not a present-but-wrong one; the deploy pipeline fills real
    contents
    ).
  • Rule 2 — Resolve media
    src
    by ref shape, then by medium.
    Fork first on ref shape: a foreign
    url
    ref
    uses
    ref.url
    directly (NO fetch, NO resolver, alt/title off the ref); a
    contentKey
    ref
    fetches the body and resolves via the toolkit's SYNC resolvers — image →
    resolveCmsImageUrl(sfdc_cms:media)
    , audio/video/document →
    resolveMediaUrl(sfdc_cms:media.url)
    (subject only, never hand-prefix). Media dispatch is by
    ref.cmsType
    and ref shape, never by body shape.
  • Rule 3 — Shared/deduped fetch state must survive React 19 StrictMode.
    useCmsItem
    dedupes via a module-level
    inflight
    map across mount→cleanup→remount: cleanup sets a local
    cancelled
    flag and MUST NOT abort the shared request (Angular's
    CmsItemService
    mirrors this). "Works once, fails on remount" is a blocking bug.
  • Rule 4 — Decode RichText before injecting, through ONE path per framework.
    decodeRichHtmlEntities
    FIRST, then inject at the single site (React
    heuristicRenderer.tsx
    ; Angular
    [innerHTML]
    in
    cms-content.component.ts
    ). Never both decode AND escape. Detection matches a tag-open (
    <tag
    /
    </
    /
    <!
    ) OR
    &lt;
    (
    RICH_TEXT_RE
    ), never a bare
    <
    or
    value.includes('<')
    .
  • Rule 5 — Type scalar fields as primitives, never
    { value }
    wrappers.
    Delivery returns scalars as PLAIN primitives (only images are objects,
    CmsImageField
    ), so read
    body.body
    , never
    body.body.value
    (the
    .value
    unwrap is always
    undefined
    ). Derive shapes from a real payload; the item
    title
    sits at the envelope ROOT, not in
    contentBody
    .
以下五条规则针对之前脚手架出现的缺陷;所有生成的应用必须满足全部五条(违反即为阻塞性bug),且已内置到
assets/
模板中——编辑模板时需保持规则有效。以下为单行摘要;完整文本参考
references/codegen-guardrails.md
。
  • 规则1——调用正确的工具包读取方法;绝不重新实现传输逻辑。
    CmsExternalRef
    →
    getCmsContentByUrl(url)
    原样调用;
    CmsRef
    →
    getCmsContentByKey
    /
    getCmsContentByKeys
    。绝不重构URL、手动映射
    contentKey
    或混淆调用路径。例外——外部
    url
    引用的独立媒体无需获取
    :其
    unauthenticatedUrl
    即为资产,直接作为元素的
    src
    /
    href
    使用(规则2);新闻/结构化
    CmsExternalRef
    仍需获取。当
    CmsRef
    需要渠道时,将解析后的channelId写入
    public/content-metadata.json
    并设置
    contents: []
    (不存在则创建;绝不覆盖已存在的渠道)——且绝不写入占位符/虚假渠道(工具包在目录缺失时会容错,但目录存在但错误时会失败;部署流水线会填充真实的
    contents
    )。
  • 规则2——按引用类型解析媒体
    src
    ,再按媒体类型解析
    。首先按引用类型分支:外部
    url
    引用
    直接使用
    ref.url
    (无需获取、无需解析器,替代文本/标题来自引用);
    contentKey
    引用
    获取body并通过工具包的同步解析器解析——图片→
    resolveCmsImageUrl(sfdc_cms:media)
    ,音频/视频/文档→
    resolveMediaUrl(sfdc_cms:media.url)
    (仅使用主题,绝不手动添加前缀)。媒体分发按
    ref.cmsType
    和引用类型划分,绝不按body类型划分。
  • 规则3——共享/去重的获取状态必须在React 19 StrictMode下保持有效。
    useCmsItem
    通过模块级
    inflight
    映射在挂载→清理→重新挂载过程中去重:清理时设置本地
    cancelled
    标志,绝不能中止共享请求(Angular的
    CmsItemService
    与此逻辑一致)。"首次有效,重新挂载失效"属于阻塞性bug。
  • 规则4——注入前解码富文本,每个框架仅通过一条路径处理。先调用
    decodeRichHtmlEntities
    ,再注入到单个位置(React的
    heuristicRenderer.tsx
    ;Angular的
    cms-content.component.ts
    中的
    [innerHTML]
    )。绝不同时解码和转义。检测需匹配标签开头(
    <tag
    /
    </
    /
    <!
    )或
    &lt;
    (
    RICH_TEXT_RE
    ),绝不匹配单独的
    <
    或
    value.includes('<')
    。
  • 规则5——标量字段类型设为原始类型,绝不使用
    { value }
    包装器
    。交付返回的标量为纯原始类型(仅图片为对象
    CmsImageField
    ),因此读取
    body.body
    ,绝不读取
    body.body.value
    (
    .value
    解析结果始终为
    undefined
    )。从真实负载推导类型;条目
    title
    位于信封根节点,不在
    contentBody
    中。

HALT behaviour

HALT行为

The skill HALTS (no write; reports the failing file, reason, and shortest fix; awaits user) on: ambiguous/unknown framework; missing/malformed markers; content with neither
unauthenticatedUrl
nor
contentKey
; ref-name collision with a different identity; render target unanswered/unresolvable; ambiguous target router; slug collision; missing detail-page prerequisite; Init/foundation drift; a cancelled/errored search (recoverable — user pastes an identity). A multi-item selection renders per Groups, not a HALT. Full catalogue:
references/failure-modes.md
.
Terminal stops — the session ENDS with nothing scaffolded (distinct from a recoverable HALT): (1) Init declined — "No" to Run Init?, so the runtime can't be scaffolded and the skill can't Embed. (2) Zero search content — experience-search-coordinate finds nothing, so the item isn't authored in this uiBundle space yet; tell the user to generate it there first, then re-run. Neither writes, installs, nor guesses. Details:
references/interaction-model.md
→ Terminal stops.
在以下情况中,技能会HALT(不写入内容;报告失败文件、原因及最短修复方案;等待用户操作):模糊/未知框架;标记缺失/格式错误;内容既无
unauthenticatedUrl
也无
contentKey
;引用名称冲突(身份不同);渲染目标未回复/无法解析;目标路由模糊;slug冲突;详情页前置条件缺失;Init/基础文件漂移;搜索取消/出错(可恢复——用户粘贴身份信息)。多条目选择按组规则渲染,不触发HALT。完整列表参考
references/failure-modes.md
。
终止性停止——会话结束,不搭建任何内容(与可恢复HALT不同):(1) 拒绝Init——对*是否运行Init?*回复"否",因此无法搭建运行时,技能无法执行Embed。(2) 搜索零内容——experience-search-coordinate未找到结果,因此该条目尚未在当前uiBundle空间中创建;告知用户先在此空间生成内容,再重新运行。两种情况均不写入、不安装、不猜测。详细信息参考
references/interaction-model.md
→终止性停止。

Init vs Embed detection

Init vs Embed检测

Against the target uiBundle root (the dir with
src/
), check every path in the detected framework's mandatory foundation set (not a single canary), then classify by how many are present (per-framework file lists + bundle-resolution rules:
references/init-scaffold.md
):
  • embed — ALL present → run the Embed pipeline.
  • init — ALL missing → ASK with options (
    references/interaction-model.md
    → Run Init?): Yes, scaffold it / No. On Yes → run Init. On No → TERMINAL (see HALT behaviour → Terminal stops): state Init was declined, write/install nothing, end the turn; do not fall through to Embed or partially scaffold.
  • drift — SOME present, SOME missing → HALT (never Embed on a partial runtime). Report the present-vs-missing lists verbatim, then ASK (
    references/interaction-model.md
    → Complete a partial runtime?): Yes, write the missing files / No. On yes → write ONLY the missing files VERBATIM (Read→Write, skips existing;
    references/init-scaffold.md
    ), then re-check; never touch or "refresh" the present ones. On no → stop. Never silently complete, overwrite, or reset a present file.
A prompt stating the runtime is already scaffolded is a strong Embed signal; still run the check, but do NOT run Init.
Embed touches ONLY the delta — never re-emit the runtime. Once Embed is detected, the run writes AT MOST the new
<framework>/types/<type>.ts
+ renderer, plus edits inside marker blocks of
shared/externalRefs.ts
and the render target. Every present foundation file stays BYTE-FOR-BYTE untouched — do not re-scaffold, re-copy, "refresh", or re-
npm install
; re-emitting the Init template (e.g. resetting
externalRefs
to
{}
) is a blocking bug. A foundation file that looks wrong → HALT as Init drift, never silently overwrite.
针对目标uiBundle根目录(包含
src/
的目录),检查检测到的框架的强制基础集中的所有路径(并非单个标记文件),然后根据存在的文件数量分类(各框架的文件列表+包解析规则参考
references/init-scaffold.md
):
  • embed——全部存在→执行Embed流程。
  • init——全部缺失→以选项形式询问用户(参考
    references/interaction-model.md
    →是否运行Init?):是,搭建脚手架 / 否。回复是→运行Init。回复否→终止会话(详见HALT行为→终止性停止):说明用户拒绝Init,不写入/安装任何内容,结束会话;不执行Embed或部分搭建。
  • 漂移——部分存在,部分缺失→HALT(绝不基于不完整运行时执行Embed)。原样报告存在/缺失文件列表,然后询问用户(参考
    references/interaction-model.md
    →是否补全不完整的运行时?):是,写入缺失文件 / 否。回复是→仅原样写入缺失文件(读取→写入,跳过已存在文件;参考
    references/init-scaffold.md
    ),然后重新检查;绝不修改或"刷新"已存在的文件。回复否→停止。绝不静默补全、覆盖或重置已存在的文件。
若提示说明运行时已搭建完成,则强烈倾向于Embed模式;仍需执行检测,但不运行Init。
Embed仅修改增量内容——绝不重新生成运行时。检测到Embed模式后,本次运行最多写入新的
<framework>/types/<type>.ts
+ 渲染器,以及修改
shared/externalRefs.ts
和渲染目标的标记块内的内容。所有已存在的基础文件保持字节级不变——绝不重新搭建、重新复制、"刷新"或重新执行
npm install
;重新生成Init模板(如将
externalRefs
重置为
{}
)属于阻塞性bug。若基础文件存在问题→按Init漂移触发HALT,绝不静默覆盖。

Bulk reads

批量读取

Per-item by default: each renderer reads independently, isolating per-card failures. To read a KNOWN set of
contentKey
s in ONE authenticated round-trip, call
getCmsContentByKeys
DIRECTLY; there is no batch-loader to scaffold. Best-effort per-key results; only
contentKey
s batch (foreign URLs read individually).
references/bulk-loading.md
.
默认逐条读取:每个渲染器独立读取,隔离单卡片失败。若要在单次认证请求中读取已知的
contentKey
集合,直接调用
getCmsContentByKeys
;无需搭建批量加载器。按键做最大努力尝试;仅
contentKey
支持批量(外部URL需单独读取)。详细信息参考
references/bulk-loading.md
。

Out of scope

超出范围

Dynamic feed discovery ("latest 10 news"); cross-ref audit tooling; media/image-reference remapping across orgs; frameworks beyond React + Angular uiBundles.
动态信息流发现("最新10条新闻");跨引用审计工具;跨组织媒体/图片引用重映射;React + Angular uiBundles之外的框架。

Files & references

文件与参考文档

Templates —
assets/shared/
:
cmsCore.types.ts
,
externalRefs.ts
,
cmsContentType.ts
(foundation + per-type body).
assets/react/
:
useCmsItem.ts
,
heuristicRenderer.tsx
(renderer +
RenderField
dispatcher),
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
.
Docs (
references/
):
package-api.md
(toolkit API + response contract — ground truth),
interaction-model.md
(question wording + terminal stops),
init-scaffold.md
(runtime tree, copy set, foundation sets),
schema-sync.md
(retrieval + regen),
verify.md
(fix policy + report),
codegen-guardrails.md
(full Rule 1–5),
embed-recipes.md
(URL regex + placement),
heuristic-render-rules.md
,
detail-pages.md
,
styling-scopes.md
,
bulk-loading.md
,
failure-modes.md
.
模板——
assets/shared/
:
cmsCore.types.ts
、
externalRefs.ts
、
cmsContentType.ts
(基础文件+按类型划分的body)。
assets/react/
:
useCmsItem.ts
、
heuristicRenderer.tsx
(渲染器+
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
。