I want a storefront for my business that works like Tools Universe (https://tools-universe.superfast.shop), a demo store built on Crystallize: Six manufacturers on typed, ETIM-aligned specs, with the battery platform as the strongest signal — and heavy equipment to rent by the day from five depots on native booking.
The content model and the wiring below are proven in that store: keep them for the features I want, and rename things to fit my trade. If a feature doesn't fit my business, leave its parts out rather than forcing it. Everything I see is mine to decide: brand, colours, typography, layout, copy and pages. Don't copy the demo's look; build my brand.
What the demo does: rental, native booking, quotes, etim, typed specs and filters, vector personalisation, stock per location. It is B2B and B2C, in English, Norwegian, Swedish, Dutch; my markets and languages may differ.
How to work with me
- You need to run commands, edit files and use MCP tools. If you can't (for example in a plain chat), tell me, and suggest I paste this prompt into Claude Code, Cursor or another coding agent instead.
- Ask your questions a few at a time (three to five), in the order of the steps below, and suggest a sensible default for each so I can just say yes. Ask each thing once, even where two lists overlap, and ask the ones that change the build first. Don't start a step before the one before it is done.
- The technical detail below is for you. Talk to me in plain words, and explain a choice only when I need to make it.
- Keep the folder empty until step 5, apart from
.env.local. - Never ask me to paste an access token secret into the chat, and tell me not to paste a filled-in command or its output either. Tell me which command to run or which file to put it in, and I'll do it.
- Before you create or change anything in Crystallize, show me the plan (shapes, pieces, topics, price variants, markets) and wait for my yes. Before an import, show me a sample of five items.
- Use the Crystallize skills (the
skillsMCP tool, or the use-crystallize plugin) for how-to:content-model,information-architecture,taxonomy,data-creation,mass-operations,pricing,query,mutation,js-api-client,responsive-images,payments, and when this store needs themsubscriptions,bookable-resources,vector-rankingandpermissions. Where this prompt and a skill disagree, the skill wins. Check the live schemas with the MCP instead of guessing field names. - When something I asked for isn't possible in Crystallize the way I pictured it, say so and offer the closest thing that is.
- The storefront never reads from the Core API on customer-facing pages. Products, public prices, stock and content come from the Discovery API and the Catalogue API; confidential prices (negotiated B2B price lists) from the Catalogue API on the server.
- Use the Shop API for everything about customers: carts, checkout, customers, orders (including orders created directly, such as renewals), order meta, pipeline stages, payments and subscription contracts. Take the customer identifier from the server session, never from the browser. The Core API is for setup scripts, back-office tools, scheduled jobs and what the Shop API can't do, always on the server and never once per page view.
Step 1: Crystallize tenant, tokens and MCP
Ask me:
- Do I have a Crystallize account and a tenant for this store? If not, I sign up for free at https://app.crystallize.com/signup and create an empty tenant. Ask for its tenant identifier.
- Do I have access tokens? If not, tell me: in the Crystallize App, go to Settings → Access Tokens → Generate a new token, name it, and keep the ID and the secret (the secret is shown once). A token acts with its user's role, so while we build, that user needs to be Tenant Admin (or have a role that can write shapes, pieces, topics, items and prices and run mass operations). Once the store is built, switch the MCP to a token from a read-only user.
- Is the Crystallize MCP connected, with write access? Check that you have its write tools (
run-mass-operation,mutate-core), not only the read tools (tenant-overview,fetch-content-model): with only the read tools, it was added without?exposeWrite=true. If it's missing, have me run this in my terminal with my own token, then restart you:
If the MCP can't be connected in my tool, say so and fall back to scripts that call the Crystallize APIs with the tokens fromnpx add-mcp "https://mcp.crystallize.com/mcp?exposeWrite=true" \ --header "X-Crystallize-Access-Token-Id: <token id>" \ --header "X-Crystallize-Access-Token-Secret: <token secret>".env.local. In Claude Code, also suggest the Crystallize skills:/plugin marketplace add crystallizeapi/ai, then/plugin install use-crystallize@crystallize-ai. The plugin brings its own read-only Crystallize MCP entry with placeholder credentials: keep using the one added above (and add&exposeSkills=falseto its URL once the plugin's skills are installed). - Confirm the connection with
tenant-overview, and tell me what's already in the tenant. If it isn't empty, ask whether to build next to what's there or in a fresh tenant.
The storefront needs the tenant identifier and its own credentials: an access token pair for the Shop API (server only), and a static auth token if Discovery and the Catalogue API are secured. Don't reuse the admin build token in production. Put them in .env.local (have me fill in the secrets) and make sure .env* is in .gitignore.
Step 2: my business and my brand
Ask me:
- What I sell, who buys it, and what makes us different. My brand name and a one-line pitch.
- My markets, currencies and languages, and my VAT/tax setup.
- Where my product data lives today: a spreadsheet or CSV, an export from Shopify, WooCommerce or another platform, a PIM or ERP, my current website, or nothing yet. Where my product images are.
- My brand: logo (SVG if I have it), colours, typefaces, tone of voice, and two or three sites whose look I like. If I have no brand guide, propose a direction from my answers and show it before building pages.
- Where it should run: my own domain, and hosting (suggest Vercel for Next.js).
Then ask what is particular to this kind of store:
- Which brands and battery platforms do I carry? The battery a trade already owns is the strongest signal in this store.
- Do I get ETIM data (or a BMEcat feed) from my suppliers, and which ETIM release? It gives typed specs and filters for free.
- Local item numbers: GTIN, and in the Nordics NOBB or NRF? (Never invent these.)
- Do I rent out equipment? Which depots, which units, rental periods and booking rules, deposits, damage waiver and site delivery?
- Trade pricing: a flat trade discount, contracts per company, volume tiers? Invoice with credit limits for trade accounts? Quote-only products?
- Which trades and materials define my customers (electrician, carpenter; concrete, wood)?
- Which margin, sales and rating data can drive ranking?
- Can I use my manufacturers' images, or do I have my own?
Step 3: the content model
Create this in my tenant with the MCP, renamed to fit my trade, after I have approved the plan. Keep the structure: it is what the storefront's features depend on.
Build in dependency order, since mass operations run in sequence with no rollback: pieces and shapes (empty first, then their components), topic maps (at most 30 topics per topic/create), price variants, markets and VAT types, the folder tree (two to four levels, five to twelve per level), and only then items. Use lowercase-hyphen identifiers. Decide now which fields are translated and which are discoverable. Restrict every item relation with acceptedShapeIdentifiers. Never add price or stock components to a shape: variants have them built in.
Every text, rich text, selection and relation the storefront shows must be discoverable; translate text if I sell in more than one language.
- Pieces:
seo(title, description, image).spec-power: power source, voltage, batteries required, motor, rated input, output power, connectivity, cable length.spec-physical: weight (with and without battery), dimensions, vibration, sound pressure and power, IP rating, LED light, belt hook.- One spec piece per product family (drilling, concrete, sawing, abrasive, woodworking, fastening, installation, measuring, extraction, battery and charging, site, storage, outdoor, consumables). Typed components only (numeric with units, boolean, selection); no properties tables. The ETIM feature code goes in each component's description. Batteries and chargers relate to the platforms they fit; consumables to the tools they fit.
- Page-builder blocks: hero, product grid (curated, topic or folder; ranked by house, personal or newest), product spotlight, category tiles, brand strip, platform picker, rental teaser, campaign banner, story, USP bar, trade call to action, video.
- tool (product):
- tagline, summary, story (paragraphs), highlights (repeatable chunk), videos.
brand(exactly one),platform(max one),technologiesrelations.powerandphysicalpieces;specsas a component choice of one family piece.in-the-box(repeatable chunk: variant SKU, quantity, item, note) on the product, keyed by variant SKU.warrantychunk (years, years if registered, registration required).launch-date; ranking numericsmargin,sold-30d,rating,review-count,campaign-boost.pairs-with(max 8),fits(tools it fits, max 75),rentalrelation → the rental version; seo.- Variant components: GTIN and local item numbers.
- rental (product): the equipment it is, tagline, summary, what's included, requirements, operator certificate needed, delivery (pickup or site delivery), deposit class, depots, damage-waiver relation. Variants are rental periods with a
duration-hourscomponent: day (24 h), weekend (68 h), week (168 h), four weeks (672 h). - service (product): damage waiver, site delivery.
- Documents: brand, platform (brand, voltage, compatible platforms, its batteries), technology, depot (market, address, location, opening hours, contact), landing-page (audience, markets, validity, a
blockscomponent multiple choice), campaign (audience, markets, priority, validity, products), guide. - category (folder). Catalogue: one root folder per category group, plus rental, brands, platforms, technology, depots, pages, campaigns, guides and services.
- Topic maps: brand, platform (plus corded and no-battery), category (group → type), trade, material, level, price tier, feature, offer (buy, rent, quote), image type, and
etim(group → class, with the ETIM class code in the topic's meta). - Vector vocabularies:
ecosystem(brand [1, 0.5], platform [1, 0.6]),work(trade [1, 0.7, 0.45], material [1, 0.6, 0.35]),gear(category [1, 0.5], level [1], price tier [0.7]). Leave corded and no-battery out. - Pricing: net prices, one price variant per currency, set so the price including VAT lands on a price point. VAT types per country.
- Customer groups per market and segment. A market
trade(all trade companies) with a percentage price list (−12 % in the demo). Contract prices: one percentage price list per company (−18 % in the demo),targetAudience: { type: SOME, customerIdentifiers: [company] }; its people inherit it. - Stock:
default(central warehouse) plus one stock location per depot. - Order pipelines: Orders, Rentals, Quotes (create pipelines and depot stock locations in the PIM API; the Shop API returns stage ids only, so keep an id-to-name map from setup).
- Give the rental folder
externalReference: folder:/rental, so code recognises it without matching paths in four languages.
Step 4: my data
Import my products and content with mass operations, mapping my data onto the model: validate each file with build-mass-operation, run it with run-mass-operation and follow it with get-mass-operation-status. Read the operation logs, not just the task status: a task can finish as complete with failed operations, and there is no rollback. Give every upsert a resourceIdentifier so a re-run updates instead of duplicating, and chunk large imports. Start with a small batch, check it in the Crystallize App with me, then run the rest.
Publish every imported item in each language: the storefront only sees published versions (in a mass operation, item/publish needs real itemIds). Import images from URLs with copyRemoteAsset, or upload them (presigned upload, then registerImage). Renditions are generated in a queue: after a large import, republish once they exist, and have the storefront fall back to the original image URL while variants is empty. Before building pages, query Discovery once; if the tenant isn't ignited yet, run igniteDiscoApi (with stacks: opensearch if we use vector ranking), wait for the task to complete, and allow a few minutes. If I have no data yet, create a small, clearly labelled placeholder set (ten or so products) so we can build the pages, and remind me to replace it.
- The demo's range was researched from manufacturer sites only, with every image URL checked. ETIM 10.0 classes and features come from the public ETIM viewer (viewer.etim-international.com) in four languages.
- Images were copied from the manufacturers' CDNs, then given alt text and topics per language. Letterboxed photos were cropped.
- Build order: tenant → model → taxonomy → images → catalogue → rental → translations → pages → vectors → customers.
- From me: my range with specs, prices and images, platforms, item numbers, depots and rental fleet, trade discounts, and ETIM classes (from an ETIM export or my suppliers' BMEcat).
Step 5: the storefront
- Next.js (App Router) on Vercel, plain
fetchGraphQL clients. The storefront reads Discovery, the Catalogue API (contract prices, on the server) and the Shop API; the Core API is for setup and back office. - Discovery API: browse with filters, topic facets, range facets (with
boundaries) on spec numerics, and pagination; search and autocomplete. Trade prices through<variant>PriceFor(marketIdentifiers: ["trade"]). Contract prices are confidential, and Discovery is public: read them on the server from the Catalogue API (priceFor(count, customerIdentifiers: [company])), cached per company. Hydrating the cart with the company applies the same list. - Shop by battery platform: a platform picker and platform pages list everything on that platform; batteries and chargers relate to the platforms they fit.
- Ranking per shopper: taste rebuilt per request from the shopper's order history (line meta carries the product's topic paths; 120-day half-life; orders count 1, rentals 0.7, quotes 0.6). Rank with
tasteCosineper vocabulary plus house boosts (sales, margin, rating, campaign boost, in stock). Alternatives withnearestTo: { vocabulary: gear }. Cart add-ons from the basket's own taste. - Rental (booked natively in Crystallize): in setup, booking policies (durations in seconds) and one
setBookablepool per rental product with named units whose meta holds the depot;publishItemafter everysetBookableorreapplyBookablePolicy. In the Shop API:availability(Discovery never serves it), thenbookSkuItemone at a time per cart, quantity 1 per unit, checking__typename. Remove a rental from the basket by re-hydrating without it;cancelReservationandrebookReservationonly work outside the cancellation window. Damage waiver and site delivery are ordinary lines added throughhydrate,type: service, in the booking line'sgroup. - Checkout: the customer goes on the cart before any booking (
hydratewith the customer, the customer's markets and meta such as the PO number) →place→ optionalconfirmCartBooking→ payment (card: create the order from the provider's verified webhook; invoice: a credit check) →/ordercreateFromCart(standard or quote, in the Orders, Rentals or Quotes pipeline) → wait for the cart's order id →confirmCartBookingagain. If payment fails after the first confirm, cancel the reservations through/booking/admin(a server token withbookingandbooking:admin). Load a company's credit limit and terms once at sign-in on the server, never per page view. - Quotes: accepting a quote creates a new order that points to it, and moves the quote to accepted.
- Env:
CRYSTALLIZE_TENANT_IDENTIFIER,CRYSTALLIZE_ACCESS_TOKEN_ID,CRYSTALLIZE_ACCESS_TOKEN_SECRET,NEXT_PUBLIC_SITE_URL.
Build my design, not a demo's: my typefaces, colours and tone, with pages that suit my products. Show me the home page, a category and a product page early, and adjust the design with me before building the rest.
Every page is built for search engines, answer engines and AI agents from the start, not added at the end:
- SEO: server-rendered HTML, one
<h1>, a unique title and description, canonical URLs,hreflangper language, a sitemap androbots.txt, and clean, translated paths. - Structured data: JSON-LD on every page that has a subject:
OrganizationandWebSite(with aSearchAction) on the home page,ProductwithOffer(price, currency, availability) on product pages,BreadcrumbListon categories and products,ArticleorRecipeon content. Generate it from the same data as the page. - GEO, for answer engines and agents:
/llms.txt(an H1, a summary, links to every section),/llms-full.txt, and a Markdown twin of every page (its URL plus.md, orAccept: text/markdown), with prices, stock and facts written out in plain sentences. - Agents can act, not only read: register the store's actions (search, add to cart, check availability) with WebMCP (
navigator.modelContext.registerTool, or annotated forms), and keep the accessibility tree clean: semantic HTML, labelled controls, real buttons and links. - Fast and stable: responsive images from Crystallize, no layout shift (reserve space for images and late content), little client JavaScript. Aim for 100 in every Lighthouse category (Performance, Accessibility, Best Practices, SEO; heavy 3D or video pages may score lower on performance) and pass every Agentic Browsing check.
Step 6: run it, then ship it
Run it locally and walk me through each feature above with my own data. Fix what I find. Before you call it done, run Lighthouse on the home page, a category and a product page (npx lighthouse <url> --only-categories=performance,accessibility,best-practices,seo,agentic-browsing), validate the JSON-LD (https://validator.schema.org), open /llms.txt and a page's .md twin, and fix what falls short. Deploy only when I ask: set the same environment variables on the host, point my domain, and give me the live URL.
Watch out for
- Vector ranking must be enabled for the tenant. Bookings need no flag, only the
bookingPoliciespermission on the token's role (FORBIDDENotherwise). Discovery answers "no ignited Tenant" until the firstigniteDiscoApi(stacks: opensearch). - A component that isn't discoverable is missing from Discovery, and rich text isn't discoverable by default. After changing that, republish; new fields can need a second ignite.
setItemTasteempties variant components on the draft: taste → rewrite variant components → publish → ignite.- Content chunks on variants don't publish: keep
in-the-boxon the product, keyed by variant SKU. - Updating with
componentsreplaces all of them: useupdateComponentfor translations.updateProductVariantwithout components empties them. - Always send
pathIdentifierwhen updating topics; translate parents first. - Item relations cap at 75. Volume tiers start at threshold 1. Chunks with translated text must be multilingual. Create shapes and pieces empty, then fill them.
- Price lists targeted at customer groups fail: target markets or customers. Prefer percentage lists; an absolute list replaces the price and loses the volume tiers.
- Booking policy durations are in seconds; set the cart's customer before
bookSkuItem. - Orders must be created and moved through the Shop API. It can't delete orders.
- Discovery:
languageis an enum literal; relations come back in the default language; autocomplete is a filter, so pair it withtermand sort by score. - Don't invent facts about my business (prices, stock, certifications, delivery promises). Ask, or leave a clearly marked placeholder.
And for every Crystallize store, from the skills:
- Orders: create and change them through the Shop API
/order(createFromCart,create,setMetawithmerge: true,addToStage,setPayments). Editing an order in Core adds a duplicate to the Shop API's list. Read a customer's orders with the identifier from the session,limit: 100, grouped bycoreId(keep the newest). Seed order history through/ordercreate, not CoreregisterOrder. The order id is the cart id; don't callfulfillaftercreateFromCart. - Payments: create the order from the payment provider's verified webhook, never from the browser, once per cart.
- Cart meta goes in
hydrate(input: { meta });hydrateis the whole cart, so send every line each time. - Images: serve them straight from Crystallize (not through an image optimizer such as
next/imagein its default setup): a WebPsrcsetfrom the returnedvariantswith an accuratesizes, falling back to the original URL while renditions are missing. Replace an image by uploading a new one, not withregisterImageRevision. - Ranking (if the store ranks per shopper): check that
__type(name: "RankByInput")exists; passcontextas a variable, withuserTasteentries of{ vocabulary, weights, magnitude }(magnitude = the square root of the sum of squared weights);tieBreakeris required; don't combinesortingwith ranking, and page withskipandlimitinside the rerank window; introspectTenantRankByFieldfor field names; leavecontextout when there's no taste; useexplain: trueonly for tuning. - Bookings (if the store books): durations are in seconds; the role needs the
bookingPoliciespermission; put the customer on the cart beforebookSkuItem; everyhydratemust resend each booked line with its window and unit, or the booking is cancelled; remove a booking by re-hydrating without it.
Done when
- My tenant has the content model above with my products, prices and images in it.
- The storefront runs on my domain in my brand, and every feature I chose works with my data: we have clicked through each one together.
- Lighthouse scores close to 100 in every category and every Agentic Browsing check passes on the home, category and product pages.
- I know how to add a product, change a price and publish, in the Crystallize App.