BizzPocket

Your first block

On this page

In about half an hour you will build a Featured collection block: a heading and a grid of products from a collection the seller picks. You will create a theme, copy a building piece into it, write the block, see it in your browser with sample products, and build it.

You need to know React. You do not need to know anything about BizzPocket, and you need no store and no token: bizzpocket dev --mock previews with sample data.

What you need

  • Bun 1.3 or newer. The BizzPocket tools run on Bun.
  • Access to the BizzPocket package registry, npm.bizzpocket.com (see registry.md).
  • Only for Step 7: a store's slug and the dev token.

Every command has --help (bunx bizzpocket dev --help).

Before publish. Until @bizzpocket/cli and @bizzpocket/theme are on the registry, bunx and bun install cannot find them. Work in a checkout of the BizzPocket repository instead: create the theme under packages/ (bun packages/cli/src/bin.ts init packages/my-theme), run bun install at the repository root, and type bun packages/cli/src/bin.ts <command> wherever this guide says bunx bizzpocket <command>. Registry errors: troubleshooting.md.

Only run themes you trust. dev and build run the theme's code on your machine, in the same process as the CLI, with your environment (tokens included). Never run them on a theme from someone you do not trust.


Step 1. Create a theme

bash
bunx @bizzpocket/cli init my-theme --blank
cd my-theme
bun install

bun install needs the registry in an .npmrc (in the folder or your home folder; drop the second line if you have no token):

ini
@bizzpocket:registry=https://npm.bizzpocket.com
//npm.bizzpocket.com/:_authToken=${NPM_TOKEN}

--blank (the default) starts from the minimal starter; --from <themeDir> copies an existing theme instead. init creates a folder with a theme that already builds. The folder name must be lowercase with dashes (my-theme). It becomes the theme's id.

my-theme/
  theme.config.ts          the theme's name, colours and fonts   (see theme-config.md)
  package.json
  blocks/
    hello/
      hello.tsx            one tiny example block
  layouts/
    global.json            blocks shown on every page (header and footer)
    home.json              blocks on the home page

The theme config holds "tokens": named design values such as colors.primary that become CSS variables.

The rule to remember: one block is one folder in blocks/, and the folder name is the block's name. The folder blocks/hello/ holds the block hello in blocks/hello/hello.tsx. There is nothing to register anywhere else.

Build it once to see it work:

bash
bunx bizzpocket build
block  server B  client gz  css gz  data
-----  --------  ---------  ------  ----
hello       292          0       0  none
total                    0       0
built .../my-theme/dist

That table is the size report. For each block it shows the server code size, the JavaScript sent to the browser (client gz), the CSS size and what kind of data the block uses. You will read it again in Step 6.


Step 2. Copy a building piece

BizzPocket publishes small pieces of source code you can copy into your theme. Like shadcn/ui, once you copy a piece it is your code: change it however you like.

bash
bunx bizzpocket add money-price
added money-price

This copied components/money-price/money-price.tsx into your theme and wrote bizzpocket.json, a small file that remembers what you installed so bizzpocket diff can later show you what changed upstream. Commit both files. More about this in registry.md.

Open the component. It is a few lines of plain React:

tsx
import { Money } from '@bizzpocket/theme';

export interface MoneyPriceProps {
  amount: number;
  currency: string;
  className?: string;
}

export function MoneyPrice({ amount, currency, className }: MoneyPriceProps) {
  return <Money amount={amount} currency={currency} className={className} />;
}

Money formats a price with Intl.NumberFormat (for example ₹1,299.00).


Step 3. Write the block

Create the folder blocks/featured-collection/ and the file blocks/featured-collection/featured-collection.tsx. The file must have the same name as the folder.

blocks/featured-collection/featured-collection.tsx
import { block, f, products, Image, Link } from '@bizzpocket/theme';
import { MoneyPrice } from '../../components/money-price/money-price';
import './featured-collection.css';

export default block({
  name: 'Featured collection',
  category: 'products',
  description: 'A heading and a grid of products from one collection.',

  // What the seller can edit. Each key becomes a prop of `render`.
  fields: {
    title: f.text({ default: 'Featured products' }),
    collection: f.collection({ label: 'Collection' }),
    count: f.number({ default: 4, min: 1, max: 12 }),
  },

  // What data the block needs. The page fetches it for you, in one request.
  data: ({ collection, count }) => ({
    items: products({ collection, first: count }),
  }),

  // `items` is the result of `products(...)`. It is null if the fetch failed.
  render: ({ title, items }) => {
    const list = items?.products ?? [];
    if (list.length === 0) return null;
    return (
      <section className="fc">
        <h2 className="fc-title">{title}</h2>
        <ul className="fc-grid">
          {list.map((p) => (
            <li key={p.id} className="fc-card">
              <Link href={`/products/${p.handle}`}>
                {p.image ? (
                  <Image src={p.image.src} alt={p.image.alt} width={400} height={400} sizes="(min-width: 768px) 25vw, 50vw" />
                ) : null}
                <h3 className="fc-name">{p.title}</h3>
              </Link>
              <MoneyPrice amount={p.price.amount} currency={p.price.currency} />
            </li>
          ))}
        </ul>
      </section>
    );
  },
});

Read it top to bottom. Every line has a job:

Part What it does
block({ ... }) Defines a block. It is the file's default export.
name, category, description What the seller sees in the page editor. category groups the block in the editor's list. It is one of header, footer, hero, products, collections, content, media, social-proof, promo, commerce, account, utility.
fields The settings the seller can edit. f.text, f.collection and f.number are field kinds (fields.md). The seller's values arrive in render as props. The labels ("Title", "Count") are made from the keys.
default The value used until the seller changes it.
data Describes the data the block needs. It does not fetch. products({ collection, first: count }) means "the first count products of collection". The page fetches it for every block at once (data.md).
render A normal React render function. It gets the field values and the data. It must be synchronous.
Image, Link Safe <img> and <a> from @bizzpocket/theme. Image sets lazy loading (and a srcset for CDN images), Link refuses unsafe URLs such as javascript:.
MoneyPrice The component you copied in Step 2.
import './featured-collection.css' The block's own CSS. Only pages that use the block load it.

Three details you would otherwise have to guess:

  • f.collection() holds a collection's slug (its URL name), or nothing if the seller has not picked one. You do not need collection ? products(...) : undefined: a missing optional argument is simply left out, and a need whose required argument is missing (a product() with no handle) is skipped and arrives as null. A ternary in data() would only make the block runtime (data.md).
  • items is what products() returns: an object whose products list holds the items ({ id, handle, title, price: { amount, currency }, image?: { src, alt } }), with total, page and totalPages next to it. The full shapes are in data.md.
  • A product's page is at /products/<handle>, so that is where the card links.

You did not write any registration, type, wrapper element or hydration code. The page adds the wrapper (data-block-type, data-block-id), validates the seller's values against fields, and renders nothing for a block that throws.

The CSS

Create blocks/featured-collection/featured-collection.css:

blocks/featured-collection/featured-collection.css
.fc { padding: 2rem 1rem; }
.fc-title { margin: 0 0 1rem; }
.fc-grid { display: grid; gap: 1rem; grid-template-columns: repeat(2, 1fr); list-style: none; margin: 0; padding: 0; }
@media (min-width: 768px) { .fc-grid { grid-template-columns: repeat(4, 1fr); } }
.fc-card img { width: 100%; height: auto; display: block; }
.fc-name { font-size: 1rem; margin: 0.5rem 0 0.25rem; }

Plain CSS. Use a class prefix that is unique to the block (fc-) because all blocks' CSS shares one page.


Step 4. Put the block on the home page, and see it

A block appears on a page when a layout lists it. Layouts are JSON files in layouts/. One command adds yours to the home page:

bash
bunx bizzpocket add-to-layout featured-collection
added featured-collection to layouts/home.json (id 3f0c…)

It appends an instance with a new id and the fields' defaults (--page product for another page). Now see it:

bash
bunx bizzpocket dev --mock
bizzpocket dev: http://127.0.0.1:4321  mock data (sample toy store, no API)

Open http://127.0.0.1:4321: the "Header" block, "Hello from your new theme", your Featured collection with sample products (a toy store: "Wooden Rainbow Stacker", "Classic Train Set"...), and the "Footer". Edit featured-collection.tsx or its CSS and save: the page rebuilds and reloads by itself. --mock needs no store and no token; every collection slug shows the same sample products. Leave it running.

To set the starting values, edit the instance in layouts/home.json. By hand, the whole file looks like this:

layouts/home.json
{
  "schemaVersion": 1,
  "pageType": "home",
  "pageIdentifier": "home",
  "themeId": "my-theme",
  "blocks": [
    {
      "id": "6f1d1f0e-9c1a-4a43-8d6a-1d2b6a0a1001",
      "type": "hello",
      "props": {},
      "visibility": { "devices": ["mobile", "desktop"] }
    },
    {
      "id": "6f1d1f0e-9c1a-4a43-8d6a-1d2b6a0a1002",
      "type": "featured-collection",
      "props": { "title": "Best sellers", "collection": "best-sellers", "count": 4 },
      "visibility": { "devices": ["mobile", "desktop"] }
    }
  ]
}
  • Copy schemaVersion, pageType and pageIdentifier as they are, and set themeId to your theme's id: the name of your theme folder (my-theme in this guide).
  • type is the folder name of the block.
  • props are the starting values. Anything you leave out uses the field's default.
  • id is any unique string. Use a new UUID for each block (add-to-layout does). Saved layouts refer to blocks by type, so do not rename a block folder once a store uses the theme.
  • With a real store (Step 7), use the slug of a collection that exists in it instead of best-sellers.
  • visibility.devices says on which screens the block shows.
  • The header and footer come from layouts/global.json. In the new theme they are two hello blocks that say "Header" and "Footer". Replace them with real blocks later (theme-config.md).

Step 5. Check the build

bash
bunx bizzpocket build
block                server B  client gz  css gz  data
-------------------  --------  ---------  ------  ------
featured-collection      1490          0     213  static
hello                     292          0       0  none
total                                  0     216
built .../my-theme/dist

Read the featured-collection row:

  • client gz is 0. The block sends no JavaScript to the browser. Only 'use client' files add to it (interactivity.md).
  • css gz is 213. That is the block's own CSS, compressed.
  • data is static. Your data() was turned into a template at build time, so the whole page's data is fetched in one call. If it said runtime there is a reason, and data.md tells you how to fix it.

Two more files in dist/ are worth a look:

  • dist/blocks.json is the catalogue the page editor reads. Open it to see your block with a Title text control, a Collection picker and a Count number control, all made from fields.
  • dist/templates.json holds the data template. You will see { "kind": "products", "args": { "collection": { "$prop": "collection" }, "first": { "$prop": "count" } } }.

If the build fails, the message ends with a link to troubleshooting.md for that error.


The block is typed without any extra step. title is a string, count is a number, collection is a string | undefined, and items is ProductGridResult | null. Add a tsconfig.json to your theme to let your editor and tsc check it:

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "jsx": "react-jsx",
    "strict": true,
    "noEmit": true,
    "skipLibCheck": true,
    "lib": ["ES2022", "DOM"]
  },
  "include": ["blocks", "components", "theme.config.ts"]
}

Install the checker once, then run it:

bash
bun add -d typescript @types/react
bunx tsc -p .

It should print nothing. (Do not run bunx tsc before installing typescript. That would download a different package that happens to be called tsc.)


Step 7. Preview it with your store's data

--mock shows sample products. To see a real store's products, menus and prices instead, you need a developer token for that store. The store owner creates one in the seller dashboard under Settings → Developers and sends it to you. It starts with bpdt_, it can only read that store's public data, and it expires.

bash
export BIZZPOCKET_DEV_TOKEN=bpdt_...   # the token the store owner sent you; never put it in a file in the theme folder
bunx bizzpocket whoami --store my-store
bunx bizzpocket dev --store my-store

whoami checks the token before you start: it prints the store's name and when the token expires, or says plainly that the token is revoked, expired or for another store.

store:   My Store (my-store)
token:   Pixel Studio (bpdt_a1B2c3D4…)
expires: 2027-01-30
bizzpocket dev: http://127.0.0.1:4321  store=my-store  api=https://api.dev.bizzpocket.com

my-store is the store's slug, its short name. Use the store you want to preview against.

Open http://127.0.0.1:4321. You see your theme rendered with the real products, menus and prices of my-store. As with --mock, saving rebuilds and reloads. dev watches blocks/, layouts/, components/ and theme.config.ts; a change anywhere else needs a restart. If a build fails, a red message appears at the bottom of the page and the last good build keeps serving.

Options:

Flag What it does
--mock Sample data from a mock API inside the CLI. No store, no token, no network. Use it instead of --store.
--store <slug> The store whose data to show (needs that store's developer token, below). One of --store or --mock is required.
--port <n> The local port. Default 4321.
--api <url> Another API. Default https://api.dev.bizzpocket.com. Must be https, or http for localhost.
--dir <path> The theme folder. Default ..
--prod Build once (server bundle minified) and serve it with no file watcher and no reload script. It is how the page behaves in production.

About the token. A developer token (bpdt_…) reads one store's public storefront data: products, categories, menus, the store profile and its pages. It cannot see orders, customers or carts, cannot change anything, and works only for the store it was made for (--store must be that store). It is limited to 120 requests a minute. When it expires or the owner revokes it, dev gets 401 and whoami tells you why to ask for a new one. Never print the token, commit it, or put it in a .env inside the theme folder: export it in your shell. The CLI sends it only to the --api host, in the x-bp-dev-token header, so only use an API you trust, and only run themes you trust: their code runs in the same process as the token. No token? dev --mock works for everything else in this guide. tokens.md explains every kind of token. BizzPocket team members can still use the dev environment's internal token in BIZZPOCKET_DEV_TOKEN; dev tells the two apart by the bpdt_ prefix.

dev renders your page with the same page code the live storefront uses. The home page shows your theme's default layouts from layouts/ for pages the store has not customised.


Step 8. Build for real

bash
bunx bizzpocket build --target both
--target Output
next The bundle for the current storefront host (Next.js), plus dist/styles.css.
bun The bundle for the Bun renderer (what dev uses), in dist/bun/.
both Both. This is the default.

Everything the build writes goes to dist/. Nothing in your source folders is rewritten. The files you edit are the files that run.

Useful files in dist/: blocks.json (editor catalogue), templates.json (data templates), size-report.json (the size table), index.d.ts and the bundles.

This release of the CLI has init, dev, add, add-to-layout, diff, build and migrate. Commands that send a theme to a store (push, publish) are not part of it yet.


Step 9. Make a part of it interactive (when you need to)

The block above sends no JavaScript. That is on purpose. When you need a button, a slider or a form that reacts, put that part in a 'use client' file in the same folder. For example, a "quick add" heart:

blocks/featured-collection/heart.tsx
'use client';
import { useState } from 'react';

export function Heart({ label }: { label: string }) {
  const [on, setOn] = useState(false);
  return (
    <button type="button" aria-pressed={on} onClick={() => setOn(!on)}>
      {on ? '♥' : '♡'} {label}
    </button>
  );
}

Import it in the block and use it like any component: import { Heart } from './heart'; then <Heart label="Save" />. Only that button is sent to the browser. The rest of the block stays plain HTML. The button becomes clickable when it comes near the screen (the default, hydrate: 'visible'); set hydrate: 'load' on the block for something that must work the moment the page loads. The full rules are in interactivity.md.


What you built

  • 1 folder, 2 files (.tsx and .css), no registration.
  • A block the seller can edit in the page editor, with typed props and defaults.
  • Product data fetched in one request with the rest of the page.
  • Zero JavaScript in the browser.

Where to go next