Content Composition
Since version 3.11
The Content Composition API allows you to create and compose custom building blocks for your content.References
API
register lifecycle hook
A method that allows you to register a new composition or extend an existing one.
Parameters
name: string- The name of the compositioncollection:CompositionCollection- An array of composition entries.
Example
export default defineExtension({
unstable_lifecycleHooks: {
onContentCompositionInit: (composition) => {
composition.register("Wysiwyg", [
{
// Since this exists, it will override the existing composition
name: "DefaultWysiwyg",
client: {
component: new URL(
"./components/DefaultWysiwyg.tsx",
import.meta.url
),
},
fragment: /* GraphQL */ `
fragment DefaultWysiwygFragment on DefaultWysiwyg {
childNodes
}
`,
},
{
// This will be added to the composition for `Wysiwyg`
name: "CustomWysiwyg",
client: {
component: new URL(
"./components/MagentoWysiwyg.tsx",
import.meta.url
),
},
fragment: /* GraphQL */ `
fragment CustomWysiwygFragment on CustomWysiwyg {
childNodes
}
`,
},
]);
},
},
});
createContentComposition helper
This method is a helper to create a Content Composition collection, which can
then be used in the onContentCompositionInit lifecycle hook to register your
composition.
Parameters
See register for more information.
Example
import { createContentComposition } from "@front-commerce/core";
const WysiwygComposition = createContentComposition("Wysiwyg", [
{
// Since this exists, it will override the existing composition
name: "DefaultWysiwyg",
client: {
component: new URL("./components/DefaultWysiwyg.tsx", import.meta.url),
},
fragment: /* GraphQL */ `
fragment DefaultWysiwygFragment on DefaultWysiwyg {
childNodes
}
`,
},
{
// This will be added to the composition for `Wysiwyg`
name: "CustomWysiwyg",
client: {
component: new URL("./components/MagentoWysiwyg.tsx", import.meta.url),
},
fragment: /* GraphQL */ `
fragment CustomWysiwygFragment on CustomWysiwyg {
childNodes
}
`,
},
]);
export default defineExtension({
unstable_lifecycleHooks: {
onContentCompositionInit: (composition) => {
composition.registerComposition(WysiwygComposition);
},
},
});
Component metadata
In addition to runtime components, each registered entry can optionally expose typed metadata — editor hints, feature flags, SEO information, documentation, anything a consumer of the composition wants to attach. The metadata channel is separate from the components channel: it lives in its own virtual module and only enters a bundle when explicitly imported.
Authoring
Metadata is declared in a sibling file named
<ComponentName>.CompositionMetadata.ts, colocated with the component:
sections/HeroBanner/
├── HeroBanner.tsx
└── HeroBanner.CompositionMetadata.ts ← sibling metadata file
The sibling exposes a named export whose name matches the scope:
<Scope>CompositionMetadata. The plugin reads this convention to wire each
entry in the virtual metadata module.
import type { SectionMetadata } from "@my-package/types";
export const SectionCompositionMetadata = {
displayName: "Hero banner",
category: "marketing",
// …whatever the package's declared shape contains
} satisfies SectionMetadata;
Filename convention
The plugin replaces the rightmost .ts or .tsx suffix of the component file
with .CompositionMetadata.ts. Casing is verbatim — case-sensitive filesystems
(Linux / CI) will not silently match compositionMetadata or similar variants.
macOS authors may get false positives on a case-insensitive filesystem that
break on CI; match the canonical casing exactly.
Typing — two-step adoption
The typing model is per-scope: the package that owns a composition scope declares the canonical metadata shape for that scope once, and every sibling under that scope conforms to it.
Step 1 — own a scope
Augment @front-commerce/types#CompositionMetadataMap in a .d.ts shipped by
the scope-owning package. The file must contain at least one top-level import
or export statement so TypeScript treats it as a module — otherwise the
declare module block silently shadows the package and breaks consumers
elsewhere:
import type { SectionMetadata } from "./SectionMetadata";
export {};
declare module "@front-commerce/types" {
interface CompositionMetadataMap {
Section: SectionMetadata;
}
}
Step 2 — author siblings
Each component's <Component>.CompositionMetadata.ts exports a constant named
<Scope>CompositionMetadata that satisfies the augmented shape (see the
authoring example above). The plugin discovers the entry keys at build time and
augments CompositionMetadataKeys automatically by emitting
.front-commerce/content-composition-metadata.d.ts during vite build /
vite dev.
Reading metadata at runtime
Three consumer APIs:
useCompositionComponentMetadata— React hook returning the whole entry map for a scope. Use it when you need to iterate over every component registered in the scope.useCompositionComponentMetadataForComponent— React hook returning a single entry's metadata, narrowed to the scope's declared shape (ornullwhen the component has no sibling metadata file).getCompositionMetadataCollection— non-React helper returning the entry map for a scope. Importable from server loaders and utilities.
import {
useCompositionComponentMetadata,
useCompositionComponentMetadataForComponent,
} from "@front-commerce/core/react";
function HeroBannerLabel() {
const meta = useCompositionComponentMetadataForComponent(
"Section",
"HeroBanner"
);
// meta: SectionMetadata | null
return <span>{meta?.displayName ?? "(unlabelled)"}</span>;
}
function SectionGallery() {
const all = useCompositionComponentMetadata("Section");
// all: { HeroBanner: SectionMetadata | null; … }
return (
<ul>
{Object.entries(all).map(([name, meta]) => (
<li key={name}>{meta?.displayName ?? name}</li>
))}
</ul>
);
}
import { getCompositionMetadataCollection } from "@front-commerce/core/content-composition";
const all = getCompositionMetadataCollection("Section");
// all: { HeroBanner: SectionMetadata | null; … }
Calling either API with a scope that the type system does not know (i.e. not
augmented in CompositionMetadataMap) is a compile-time error at the call site.
Scope ownership rule
One package owns each scope's metadata shape. If you need a different metadata
shape, register a different scope name — do not augment an existing scope owned
by another package. TypeScript merges interface declarations; conflicting
field types produce Subsequent property declarations must have the same type
errors at the consumption site, far from the offending augmentation.
The package that first calls composition.register("<scope>", ...) is the
conventional owner. Downstream extensions adding components to the same scope
must conform to the owner's shape via the same <Scope>CompositionMetadata
named export.