Write the normalize transform
Bridge a binding that emits a different def than your transform consumes.
The transform module has three entry points fixed by the runtime: normalize, materializeRecords, and buildView. normalize is optional and exists for one situation: a binding whose emits is not the def your transform takes as input.
When you need it
transform.input is the def the transform consumes. When every transform-feeding binding the package pins emits the same def, input is that def and can be omitted. When they differ, declare input and export normalize for every binding whose emits is something else:
"transform": {
"input": "com.example.blog.post@^1.0.0#extract",
"hooks": { "file": "blog-plugin.js" },
"records": ["com.example.blog.post@^1.0.0"]
}The registry refuses a manifest where neither holds.
Write it
normalize receives items in the binding's emitted shape and returns items in the input shape:
import type { ContentPipelineModule } from 'workspace.packages.feed-package-runtime';
const plugin = {
normalize(items, context) {
if (context.binding !== 'legacy-archive') return items;
return items.map((item) => ({
...item,
fields: {
title: item.fields['headline'],
href: item.fields['link'],
date: item.fields['published'],
},
}));
},
materializeRecords(items) { /* ... */ },
buildView({ handler, root, records }) { /* ... */ },
} satisfies ContentPipelineModule;
export default plugin;The output is validated against transform.input before materializeRecords runs. Items that do not conform are dropped and reported as rejections rather than failing the capture, so watch the validation summary's rejection counts after adding a normalizer.
Keep the rest pure
All three entry points run in the capability-free sandbox: no host, no network, no filesystem, no timers. Conversions that need URL, Intl, or base64 come from the declared stdlib. See Plugin runtime environment.
Reference: transform, transformHooks.