# Storefront (/apps/storefront)

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 [#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](/apps/ui#settings-and-translations).



```ts
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' }),
  },
});
```



```tsx
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.

## Header and footer zones [#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.



```tsx
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 [#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](/apps/manifest#storefrontpages), 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.



```tsx
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 [#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 [#next]

- [UI](/apps/ui): every component and hook a worker can use.
- [Styling](/apps/styling): how workers take the store's look.
