| name | recipe-authoring |
| description | Author Sitecore recipes (.recipe.ts) that compile to templates + renderings — map a React/SXA component to a component-template, model data with content-templates, define enums, group with component-sections. Use when creating or editing *.recipe.ts files, turning a component into a Sitecore rendering, or asking "how do I make a recipe / why doesn't my component show up in Pages". |
Recipe Authoring
A recipe is a TypeScript object describing one thing you want in Sitecore.
scai provision recipe push compiles the set to idempotent Authoring operations
and applies them. Re-running is a no-op.
Quick start
scai provision recipe compile -n <env>
scai provision recipe diff -n <env>
scai provision recipe push -n <env> --what-if
scai provision recipe push -n <env> --allow-write
scai provision recipe roots -n <env>
Import the kind type straight from the package — defaulted fields are optional, so
you write only what you mean:
import type { ComponentTemplateRecipe } from "@sitecoreai-labs/sitecoreai-cli/recipe";
export default {
kind: "component-template",
schemaVersion: "1" ,
} satisfies ComponentTemplateRecipe;
Stable kinds on /recipe: ComponentTemplateRecipe, ContentTemplateRecipe,
EnumerationRecipe, ComponentSectionRecipe, DesignParametersTemplateRecipe,
PageTemplateRecipe, VariantRecipe. Composition kinds (PartialDesign,
PageDesign, Site, ContentItem, Dictionary) live on /recipe/unstable.
Authoring checklist
- One recipe per thing. Each placeable component → one
component-template.
Each reusable data shape → one content-template. Each value list → one
enumeration.
- Identity.
handle: "<kebab-name>@<major>" (e.g. hero@1) — load-bearing,
seeds every GUID; never rename to change meaning, bump @2 for a new identity.
name must match the component-map key / SDK component export (e.g. Hero).
displayName is the Title-Case label authors see.
- Props →
fields. Map each editable prop to a field shape (table below) and
override with sitecore: { type, required, hint, section, sortOrder, defaultValue }.
Group with section ("Content", "Action", "Style") and order with sortOrder
(100-step increments; styles at 200+).
- Style / layout choices →
params, not fields. A color/size/position
prop is a rendering parameter. Use shape: "enum" with either inline
values: [...] + sitecore: { type: "droplist" }, or sitecore: { enumHandle: "<enum>@1" } pointing at a shared enumeration recipe. Extract shared param sets
into a _params.ts module and spread them.
- Repeating lists → a
content-template + a reference field. Don't hard-code
arrays. Model the item as a content-template, then a reference (multiple)
field with sitecore: { source: { kind: "filter", types: ["<item>@1"] } }
(a Treelist). Optionally also list the handle in insertOptions so authors can
create the items as children of the datasource (child-item pattern).
- Variants are PascalCase and map 1:1 to React named exports (
Default,
FullWidth). The SDK looks them up case-sensitively.
- Containers vs leaves. A wrapper that holds other renderings declares
placeholders: [{ key, allowedComponents: [...] }] and renders
<Placeholder name={key} />. A leaf lists the placeholder keys it can drop into
in placedIn: ["headless-main", …].
- ⚠️ Every component-template MUST reference a
component-section via
section: { handle: "<section>@1" }. Without it, the available-renderings
aggregate skips the component and it never appears in the Pages component
list — even though the rendering exists. Author one ComponentSectionRecipe
per group and point every component at it.
- Cross-references resolve by handle at compile time (deterministic GUIDs);
compile fails INPUT_INVALID on an unresolved handle.
- Organize + verify. Recipes must live in a subdirectory of the glob
(
recipes/**/*.recipe.ts does not match files at the glob root). Keep shared
enums/sections/params in their own folders. Always compile before push.
Field shapes
| Prop (React) | shape | Default Sitecore type | Notes |
|---|
string heading/label | text | Single-Line Text | |
rich body / ReactNode | richText | Rich Text | |
imageSrc + imageAlt | image | Image | |
{ label, href } | link | General Link | |
boolean flag | boolean | Checkbox | |
| number | number / integer | Number / Integer | |
"a" | "b" union | enum | Droplink | inline → set sitecore.type: "droplist"; shared → sitecore.enumHandle |
| list of items | reference (multiple: true) | Treelist | sitecore.source: { kind: "filter", types: ["<item>@1"] } |
reference with multiple: false → Droplink. Override any default with
sitecore.type.
Examples
Component template — fields + params + treelist + variants + section + placement:
import type { ComponentTemplateRecipe } from "@sitecoreai-labs/sitecoreai-cli/recipe";
export default {
kind: "component-template",
schemaVersion: "1",
handle: "hero@1",
name: "Hero",
displayName: "Hero",
section: { handle: "sodra@1" },
fields: [
{
name: "Title",
shape: "richText",
sitecore: { required: true, section: "Content", sortOrder: 100 },
},
{ name: "Intro", shape: "richText", sitecore: { section: "Content", sortOrder: 200 } },
{
name: "Buttons",
shape: "reference",
multiple: true,
sitecore: { source: { kind: "filter", types: ["nav-link@1"] } },
},
],
insertOptions: ["nav-link@1"],
params: [
{
name: "Color",
shape: "enum",
values: ["Grey", "Yellow", "Green"],
default: "Grey",
sitecore: { type: "droplist", section: "Style", sortOrder: 200 },
},
],
variants: [{ name: "Default" }],
placedIn: ["headless-main"],
meta: { tax: { group: "components", subgroup: "hero" } },
} satisfies ComponentTemplateRecipe;
Content template — a reusable child shape (no rendering):
export default {
kind: "content-template",
schemaVersion: "1",
handle: "nav-link@1",
name: "NavLink",
displayName: "Nav Link",
fields: [
{ name: "Label", shape: "text", sitecore: { required: true } },
{ name: "Link", shape: "link", sitecore: { required: true } },
],
} satisfies ContentTemplateRecipe;
Component section — the toolbox group every component points at:
export default {
kind: "component-section",
schemaVersion: "1",
handle: "sodra@1",
name: "Sodra",
displayName: "Södra",
} satisfies ComponentSectionRecipe;
Enumeration — shared droplist values referenced via enumHandle:
export default {
kind: "enumeration",
schemaVersion: "1",
handle: "button-variant@1",
name: "ButtonVariant",
displayName: "Button Variant",
default: "default",
values: [
{ name: "default", displayName: "Default" },
{ name: "outline", displayName: "Outline" },
],
} satisfies EnumerationRecipe;
Placeholders
Two directions — keep them straight:
- EXPOSE a slot (container components): put
placeholders: [{ key, displayName?, folder?, dynamic?, allowedComponents: [...] }] on the component-template and
render <Placeholder name={key} rendering={rendering} />. For a site-chrome slot
that belongs to no single component (/header, /footer, headless-main),
author a standalone kind: "placeholder" recipe (PlaceholderRecipe, on
/recipe/unstable) with key, name, allowedComponents.
- PLACE INTO a slot (leaf components): list the keys in
placedIn: ["headless-main", …].
Allowed Controls = the union of a placeholder's allowedComponents and every
component that names that key in placedIn. Either side grants placement — you
don't need both.
- Static vs dynamic. A static
key is one slot per page. For a container that
repeats (appears multiple times, or holds N children), set dynamic: true on the
placeholder and dynamicPlaceholders: true on the host component-template —
SXA appends -{uid}-{index} per instance. Registry placedIn patterns use a
{*} wildcard suffix (headless-main-{*}) to match those dynamic keys.
- Resolution. Keys a recipe in the set declares converge in one push (the
placeholder-settings aggregate). Keys with no recipe declaration are patched onto
the tenant's existing Placeholder Settings after push. Per-site settings land
under
placeholderSettingsCreate (derived from site/siteCollection).
placeholders: [
{ key: "sodra-grid", displayName: "Section Grid Tiles",
allowedComponents: ["text-with-link@1", "text-and-image@1"] },
],
Datasources
A component-template's fields are its datasource template. The optional
datasource block controls where instances live and how the add-to-page dialog
behaves:
datasource: {
template: { handle: "destination-card@1" },
autoCreate: true,
openPropertiesAfterAdd: false,
locations: [
{ scope: "page", subfolder: "Destinations" },
{ scope: "site", subfolder: "Destinations" },
],
query: ["fast:/sitecore/content/…//*[@@templatename='Foo']"],
}
scope: "page" → datasource items live under the page (./Data/<subfolder>);
scope: "site" → a shared pool under contentItemsRoot. Offer both so authors
pick where content lives.
allowedTemplates on a location constrains that subfolder's Insert Options to
specific handles (one rendering, several subfolders, each with its own allow-list).
- Two ways to model child/related content (they can coexist):
- Treelist (reference): a
reference field with source: { kind: "filter", types: ["<item>@1"] } — author picks existing items from anywhere.
- Child items:
insertOptions: ["<item>@1"] — author creates the items as
children of the datasource. List the same handle in both to support either flow;
the React component reads whichever path the layout service delivers.
- A static component with no author-pickable data omits
datasource entirely.
Gotchas (hard-won)
- No
section → invisible in Pages. The #1 "my component doesn't show up" cause.
Add a component-section and section: { handle } on every component-template.
- Glob needs a subfolder.
src/recipes/**/*.recipe.ts won't match a recipe at
the glob root — put recipes under blocks/, _content/, _sections/, etc.
name must equal the component-map key, or the SDK renders the
missing-component fallback. Variants must be exact-case React exports.
recipe compile is set-aware — it resolves section/treelist/enum handles
across the whole glob and emits the available-renderings + placeholder-settings
aggregates to .scai/. A single-file --input of a recipe that references a
section in another file can't resolve it; compile the set.
- Inline
enum params need sitecore.type: "droplist" (inline Droplink isn't
supported) — or point at a shared enumeration with enumHandle.
- Tree paths derive from
site + siteCollection on the env profile (no
hand-written recipeRoots needed). Inspect with scai provision recipe roots.