Your first block
On this page
- What you need
- Step 1. Create a theme
- Step 2. Copy a building piece
- Step 3. Write the block
- The CSS
- Step 4. Put the block on the home page, and see it
- Step 5. Check the build
- Step 6. Check the types (optional but recommended)
- Step 7. Preview it with your store's data
- Step 8. Build for real
- Step 9. Make a part of it interactive (when you need to)
- What you built
- Where to go next
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
bunx @bizzpocket/cli init my-theme --blank
cd my-theme
bun installbun install needs the registry in an .npmrc (in the folder or your home folder; drop the second line
if you have no token):
@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 pageThe 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:
bunx bizzpocket buildblock server B client gz css gz data
----- -------- --------- ------ ----
hello 292 0 0 none
total 0 0
built .../my-theme/distThat 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.
bunx bizzpocket add money-priceadded money-priceThis 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:
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.
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 needcollection ? products(...) : undefined: a missing optional argument is simply left out, and a need whose required argument is missing (aproduct()with nohandle) is skipped and arrives asnull. A ternary indata()would only make the block runtime (data.md).itemsis whatproducts()returns: an object whoseproductslist holds the items ({ id, handle, title, price: { amount, currency }, image?: { src, alt } }), withtotal,pageandtotalPagesnext 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:
.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:
bunx bizzpocket add-to-layout featured-collectionadded 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:
bunx bizzpocket dev --mockbizzpocket 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:
{
"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,pageTypeandpageIdentifieras they are, and setthemeIdto your theme's id: the name of your theme folder (my-themein this guide). typeis the folder name of the block.propsare the starting values. Anything you leave out uses the field'sdefault.idis any unique string. Use a new UUID for each block (add-to-layoutdoes). Saved layouts refer to blocks bytype, 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.devicessays on which screens the block shows.- The header and footer come from
layouts/global.json. In the new theme they are twohelloblocks that say "Header" and "Footer". Replace them with real blocks later (theme-config.md).
Step 5. Check the build
bunx bizzpocket buildblock server B client gz css gz data
------------------- -------- --------- ------ ------
featured-collection 1490 0 213 static
hello 292 0 0 none
total 0 216
built .../my-theme/distRead the featured-collection row:
client gzis0. The block sends no JavaScript to the browser. Only'use client'files add to it (interactivity.md).css gzis213. That is the block's own CSS, compressed.dataisstatic. Yourdata()was turned into a template at build time, so the whole page's data is fetched in one call. If it saidruntimethere is a reason, and data.md tells you how to fix it.
Two more files in dist/ are worth a look:
dist/blocks.jsonis the catalogue the page editor reads. Open it to see your block with aTitletext control, aCollectionpicker and aCountnumber control, all made fromfields.dist/templates.jsonholds 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.
Step 6. Check the types (optional but recommended)
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:
{
"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:
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.
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-storewhoami 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-30bizzpocket dev: http://127.0.0.1:4321 store=my-store api=https://api.dev.bizzpocket.commy-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
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:
'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 (
.tsxand.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
- All field kinds and options: fields.md
- Every data helper, and how to keep data fast: data.md
- Interactive parts: interactivity.md
- Theme colours, fonts and looks: theme-config.md
- More building pieces: registry.md
- Something failed: troubleshooting.md