Build a Znode 10 Page Builder widget for preview and storefront
Build a resource-panel widget whose saved fields, editor controls, and storefront rendering stay aligned across both Next.js applications.
Try the resource-panel widget
Edit the fields, save example content, and click through to the included checklist. This uses the recipe’s React renderer and local-path validator.
Open interactive preview
In this guide
- Editor fields
- Saved configuration
- Shared view model
- Storefront render
A widget has two audiences
A marketer adds a technical-resources panel in Page Builder. It looks correct in preview, so they publish it. On the storefront, the heading falls back to a default and one link disappears. The component works in isolation; the contract between the editor and renderer does not.
Build the widget around the saved content it owns. For this resource panel, that is a heading, one link label, and one local path. A single resource action keeps the first implementation useful and bounded, with a clear path for adding a list later through a deliberate saved-contract change.
Implement the shared widget
Create ResourcePanelConfig.tsx in the theme's widgets/ui-widgets/resource-panel folder. ResourcePanelProps defines the three saved values: heading, actionLabel, and actionPath. ResourcePanel renders those values, and ResourcePanelConfig connects them to Page Builder's fields and defaults.
This import requires @puckeditor/core 0.21.1 and the complete Puck migration described by Znode, including existing page data. On an older branch using @measured/puck, complete that migration in both Next.js applications before adding this component. Keep the saved component key ResourcePanel unchanged.
widgets/ui-widgets/resource-panel/ResourcePanelConfig.tsx tsx
import type { ComponentConfig } from "@puckeditor/core";
export type ResourcePanelProps = {
heading: string;
actionLabel: string;
actionPath: string;
};
export function localPath(value: string): string | null {
if (!value.startsWith("/") || value.startsWith("//") ||
/[\\\s]/.test(value)) return null;
const base = new URL("https://store.example.test");
const target = new URL(value, base);
return target.origin === base.origin
? target.pathname + target.search + target.hash : null;
}
export function ResourcePanel(props: ResourcePanelProps) {
const path = localPath(props.actionPath);
return (
<section className="gcg-resource-panel">
<h2>{props.heading}</h2>
{path && props.actionLabel.trim() &&
<a href={path}>{props.actionLabel}</a>}
</section>
);
}
export const ResourcePanelConfig: ComponentConfig<ResourcePanelProps> = {
label: "Technical resource panel",
fields: {
heading: { type: "text", label: "Heading" },
actionLabel: { type: "text", label: "Link label" },
actionPath: { type: "text", label: "Local link path" }
},
defaultProps: {
heading: "Technical resources",
actionLabel: "View documents",
actionPath: "/resources"
},
render: (props) => <ResourcePanel {...props} />
};
For actionPath '/resources?type=manual', localPath returns the same local path and the component displays the link. It rejects a protocol-relative address such as '//other.example' and hides the action. An empty actionLabel also hides the link, while the saved heading still renders.
Add defaults without overwriting editorial work
Defaults make a newly inserted widget useful. They should preserve existing saved content. The example supplies a default heading and resource action when a widget is inserted. Its renderer uses the actual saved values, including a deliberately cleared label.
When extending an existing widget, preserve its saved key and supported properties unless you are explicitly migrating configuration. Changing a registration name can strand existing content. Adding a required field without a fallback can break pages created before the release. Test an older serialized configuration alongside a newly created widget, and document the supported migration if the contract must change.
Wire both applications deliberately
Add the ResourcePanel entry to your theme's existing config/override-component-list.ts. The small export below is the entry to merge into addOrOverrideComponents; retain the theme's other entries, root configuration, and loader.
Deploy the shared component and registration to both Page Builder and Webstore. The editor saves the ResourcePanel key, and the storefront must resolve that same key to the same component. A component imported into only one application cannot complete that journey.
This panel links to public resources. If you later add account-specific documents, load them through an authorized server request. Keep protected document data out of publicly cached page configuration.
config/resource-component-entry.ts typescript
import { ResourcePanelConfig } from "../widgets/ui-widgets/resource-panel/ResourcePanelConfig";
export const resourceComponentEntry = {
ResourcePanel: ResourcePanelConfig
};
// Spread resourceComponentEntry into the existing addOrOverrideComponents map.
// Preserve all other registrations in that theme.
Walk through the complete editorial journey
Create a page with the widget, change every field, save, leave the editor, and reopen it. Verify the exact values survived. Preview the page, publish through the target environment's supported workflow, and inspect the public storefront in a separate session. Check headings, the resource action, missing labels, and links.
Also test a cleared action label, an older saved configuration, an invalid path, long labels, and a narrow viewport. Navigate every link with a keyboard. If the widget contains interaction, verify focus and reduced-motion behavior where relevant. Include this editorial journey in the paired Page Builder and Webstore release checks.
Keep the first release easy to maintain
Package the saved schema, renderer, defaults, and tests together. Record the SDK and theme versions with the release so the next upgrade has an identifiable comparison point. A screenshot is useful evidence of appearance, but preserve the configuration fixture too; it explains what produced that appearance.
Add richer features only after the basic lifecycle is reliable. A useful widget is more than a visually convincing preview. It is content that an editor can understand, save, revisit, and publish with predictable results across the applications that consume it.
Type-check the widget with the application settings
Place this overlay beside a tsconfig.json that extends the installed application's configuration and can resolve the recipe's widgets and config paths. Run npx tsc -p tsconfig.resource-panel.json for the Page Builder configuration and again for the Webstore configuration. It enables strict checking and emits no JavaScript. Use the TypeScript, React, and Puck versions declared in the guide, and retain the application's aliases and module settings.
tsconfig.resource-panel.json json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"strict": true,
"noEmit": true,
"jsx": "react-jsx",
"forceConsistentCasingInFileNames": true
},
"include": [
"widgets/ui-widgets/resource-panel/ResourcePanelConfig.tsx",
"config/resource-component-entry.ts"
],
"exclude": ["node_modules"]
}
References and further reading
Independent guidance from GCG. Znode is a trademark of its owner. Examples use fictional data and are not official platform documentation. Suggest a correction.