Developers
Start building
Apps/Storefront

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 with read, add, update and remove, 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.

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.header and layout.footer zones;
  • the /apps/<slug> routes inside its layout.

Next

  • UI: every component and hook a worker can use.
  • Styling: how workers take the store's look.
Last updated