Storefront
Where an app appears on the storefront. Blocks merchants place in the Customizer, header and footer zones, storefront pages, the cart, and what a theme must support.
On this page
On the storefront, an app draws native workers in three kinds of place: blocks the merchant places on a page
template, the theme's header and footer zones, and whole storefront pages at /apps/<slug>.
A worker appears only where the merchant put it in the published theme, and only while the app is installed. Removing or moving the block, changing its settings or the app's release, or uninstalling the app takes it off the page; publishing unrelated theme changes does not. If a worker fails to load, shoppers see nothing in its place rather than an error.
Blocks
Workers with the targets storefront.home, storefront.products, storefront.product, storefront.cart,
storefront.checkout, storefront.login, storefront.register, storefront.account, storefront.contact and
storefront.page are blocks for that page template. In the Customizer, every shipped theme lets merchants add
them to its sections on those templates.
Each block describes itself with defineBlock from @poltura/app-sdk/elements: the name, icon and category in
the Customizer's picker, its settings and the style controls merchants get. List the files that hold the
definitions in elements in the manifest and the block's type in the worker's element; the build reads them
into the release. See Settings and translations.
export const recommendationsPanelElement = defineBlock({
type: 'recommendations-panel',
label: 'Recommendations',
settings: {
heading: field.text({ label: 'Heading', default: 'Recommendations' }),
max_items: field.number({ label: 'Item limit', min: 1, max: 8, default: 4 }),
},
});
export const recommendationsElement = defineBlock({
type: 'recommendations',
label: 'You may also like',
description: 'Recommended products next to the current product.',
icon: 'product-grid',
category: 'commerce',
settings: {
heading: field.text({ label: 'Block heading', default: 'You may also like' }),
variant_id: field.text({
label: 'Featured variant',
default: '0199f3a2-af74-7051-b24d-6f8b1c3d4e5f',
}),
},
style: ['spacing', 'background', 'border'],
presets: [
{
id: 'quiet',
label: 'Quiet',
settings: { heading: 'More to explore' },
},
],
});
export const newsletterCaptureElement = defineBlock({
type: 'newsletter-capture',
label: 'Newsletter capture',
icon: 'mail',
category: 'forms',
style: ['spacing', 'background'],
});
export const recommendationsPageElement = defineBlock({
type: 'recommendations-page',
label: 'Recommendations page',
settings: {
heading: field.text({ label: 'Page heading', default: 'Recommendations' }),
},
});export function StorefrontProduct(): ReactNode {
const copy = copyFor(useLocale());
const settings = useSettings();
const theme = useTheme();
const cart = useCart();
const toast = useToast();
const host = useStorefrontContext();
const recommend = useAction('recommend');
const heading = String(settings.heading ?? copy.alsoLike);
const storeName = theme?.settings?.store_name;
const [contextLabel, setContextLabel] = useState(copy.alsoLike);
useEffect(() => {
void host.read().then(async (context) => {
if (context.kind === 'product') setContextLabel(context.product.title);
const suggestion = await recommend({
productId: context.kind === 'product' ? context.product.id : null,
});
const title = z.object({ title: z.string().min(1) }).safeParse(suggestion);
if (title.success) setContextLabel(title.data.title);
});
}, [host, recommend]);
return (
<Card className='offer-panel rounded-theme bg-surface text-foreground p-4'>
<Stack>
<Heading className='font-display'>{heading}</Heading>
<Text>{z.string().min(1).safeParse(storeName).data ?? contextLabel}</Text>
<Button
label={copy.addToCart}
className='bg-primary text-on-primary'
onPress={async () => {
const parsed = VariantId.safeParse(settings.variant_id);
if (!parsed.success) {
await toast.show({ tone: 'warning', message: copy.cartRejected });
return;
}
const reply = await cart.add({ variantId: parsed.data, quantity: 1 });
if (reply.status === 'ok') {
await toast.show({ tone: 'success', message: copy.added });
return;
}
await toast.show({ tone: 'warning', message: copy.cartRejected });
}}
/>
</Stack>
</Card>
);
}useStorefrontContext().read()tells the block where it is:{ kind: 'product', product, selectedVariantId? }on a product page,{ kind: 'collection', collection }on a collection page,{ kind: 'cart', cart }on the cart,{ kind: 'page', path }elsewhere, or{ kind: 'none' }.useCart()reads and changes the shopper's cart withread,add,updateandremove, 1 to 99 of an item at a time. An optional idempotency key (up to 64 letters, digits,_and-) makes a retried change apply once. Cart calls work only on the storefront.useAction('recommend')calls your server, signed by Poltura, the same way an admin worker does. Storefront calls also carry the shopper.
Workers cannot read app storage directly. The example keeps a featured variant in the block's settings, and its server keeps the full list in storage.
Header and footer zones
Workers with the targets storefront.layout.header and storefront.layout.footer appear in the theme's header and
footer. Every shipped theme has both zones. A header zone sits in its own full-width row under the navigation,
so a taller worker pushes the page down instead of covering it.
export function StorefrontFooter(): ReactNode {
const copy = copyFor(useLocale());
const toast = useToast();
const [email, setEmail] = useState('');
const [error, setError] = useState('');
return (
<Card className='rounded-theme bg-muted text-foreground p-4'>
<Stack>
<Text>{copy.footerHelp}</Text>
<TextField
label={copy.emailLabel}
value={email}
error={error}
onChange={(value) => {
setEmail(value);
setError('');
}}
/>
<Button
label={copy.subscribe}
onPress={async () => {
const parsed = Email.safeParse(email);
if (!parsed.success) {
setError(copy.invalidEmail);
await toast.show({ tone: 'warning', message: copy.invalidEmail });
return;
}
setError('');
await toast.show({ tone: 'success', message: copy.subscribed });
}}
/>
</Stack>
</Card>
);
}The footer form checks the address in the browser and sends it nowhere.
Storefront pages
A worker with the target storefront.app-page becomes a page of the store, at /apps/<slug>, once it is listed
in storefrontPages in the manifest, up to 8 of them. It is left out of the sitemap
unless seo.indexable is true. Merchants find the pages in the Customizer and can link them from menus.
export function AppPage(): ReactNode {
const copy = copyFor(useLocale());
const settings = useSettings();
const navigate = useNavigate();
const heading = String(settings.heading ?? copy.heading);
return (
<Card className='rounded-theme bg-surface text-foreground p-4'>
<Stack>
<Heading>{heading}</Heading>
<Text>{copy.pageIntro}</Text>
<Button label={copy.shopProducts} onPress={() => navigate.to('/products')} />
</Stack>
</Card>
);
}Without a storefrontPages entry the worker ships but no address serves it.
What a theme must support
Every theme Poltura ships supports apps. A theme you build yourself needs, for apps to work in it:
- sections that accept any block (
ELEMENT_WILDCARD) on the templates above; - the
layout.headerandlayout.footerzones; - the
/apps/<slug>routes inside its layout.