Freeframe

Documentation

Market

v1.0.2Updated Oct 16, 2026A complete online store for a handmade homeware brand, with a working cart and checkout flow.

Overview#

Market is a complete online store for a handmade homeware brand, built as a WordPress block theme. The fictional brand is Kiln & Co., a ceramics studio in Portland, Oregon. The theme has a home page with collections, bestsellers, reviews and a newsletter band; a shop with category, price and sort filters; product pages with glaze and size variants; a cart saved in the browser with a slide-out mini cart and a promo code; a validated demo checkout; an about page; a searchable FAQ with a contact form; and a 404 page. It is version 1.0.0 (see template.json and style.css), works with WordPress 7.1 and PHP 7.4 or newer, and is the WordPress version of the HTML template of the same name. See it live at freeframe.dev/templates/market.

The pages around the store (headings, copy, collections, reviews, the about and FAQ pages, buttons) are core WordPress blocks arranged in block patterns, so you edit them in the block editor without touching code. The store itself works as in the HTML version: the products are one JavaScript list in assets/js/main.js, and the script draws the product cards, the product page, the cart and the checkout from it. It is a storefront for a demo or a prototype; to take real orders, see Checkout. Colours and fonts are design tokens in theme.json: change them in Appearance > Editor > Styles and the whole site follows.

Quick start#

Requirements: WordPress 7.1 or newer and PHP 7.4 or newer. No plugins are needed.

  1. Download market.zip from freeframe.dev/templates/market (the WordPress tab).
  2. In your WordPress admin go to Appearance > Themes > Add New Theme > Upload Theme, choose the zip and click Install Now, then Activate. The theme is listed as Freeframe Market.
  3. Go to Appearance > Market setup and click Create the demo pages. This creates the Home, Shop, Product, Cart, Checkout, About (Our studio) and FAQ pages from the theme's page patterns, sets Home as the front page (Settings > Reading) and creates the header menu (Shop all, Vases, Mugs & Cups, Our studio, Help). Pages that already exist with the same slug are left alone, so it is safe to click it again.
  4. Visit your site. Edit any page in Pages, and the header, footer and templates in Appearance > Editor.

You can also try the theme without installing anything: the Try it in WordPress button on freeframe.dev opens it in WordPress Playground (WordPress running in your browser) with the demo pages already set up.

To work on the theme's files locally you need Node.js 24 and pnpm; WordPress itself runs in WordPress Playground, so there is no PHP, web server or database to install:

sh
pnpm install
pnpm dev     # builds the CSS, then starts WordPress with the theme at http://127.0.0.1:9400

pnpm dev signs you in as admin (password password) and creates the demo pages. The site is rebuilt from scratch every time you start it; your edits to theme files are live, edits made in the WordPress admin are not kept.

Pages#

Each page is a page pattern (in Patterns > Market pages) made of section patterns. Every section is also offered on its own in the block inserter under Patterns > Market.

Page Slug Page pattern Sections (pattern files in patterns/)
Home / page-home.php home-hero (with product art), press, collections, bestsellers (product cards), process, promises, reviews, newsletter-band
Shop /shop/ page-shop.php shop-hero, shop-catalogue (filters, sort and product grid), shop-promises
Product /product/?id=ridge-vase page-product.php product-breadcrumb, product-details (gallery, options, add to cart, details), product-reviews, related
Cart /cart/ page-cart.php cart-header, cart-empty, cart-full (lines, order summary, promo code)
Checkout /checkout/ page-checkout.php checkout-header, checkout-done (confirmation), checkout-empty, checkout-main (form and summary)
About /about/ page-about.php about-hero, about-gallery, story, timeline, team, visit
Help & FAQ /faq/ page-faq.php faq-hero (with search), faq-list (topics and answers), faq-contact (contact form and details)
404 any missing URL templates/404.html 404

The script finds the shop, product, cart and checkout pages by their slugs (shop, product, cart, checkout): if you rename one, change its URL in freeframe_market_page_urls() in inc/setup.php too. Posts use templates/single.html; the blog, archives and search results use templates/index.html, templates/archive.html and templates/search.html.

Project structure#

freeframe-market/
  style.css                 theme header (name, version, requirements, license); no styles
  theme.json                design tokens: palette, fonts, font sizes, spacing, layout widths, button
  functions.php             loads the files in inc/
  inc/setup.php             stylesheet and script (with its strings and page URLs), editor styles,
                            the `js` class script, pattern categories
  inc/icons.php             registers assets/icons/*.svg for the Icon block
  inc/icon-labels.php       the icons' names shown in the editor
  inc/block-styles.php      button block styles
  inc/site-setup.php        Appearance > Market setup: creates the demo pages and the menu
  templates/                page.html (pages), page-with-title.html, single.html, index.html,
                            archive.html, search.html, 404.html
  parts/                    header.html and footer.html (they show the header and footer patterns)
  patterns/                 every section and page (PHP files with block markup)
  assets/css/theme.css      the compiled stylesheet (do not edit: built from src/)
  assets/css/editor.css     the same for the block editor (generated by src/editor-css.mjs)
  assets/js/main.js         the store: products, product art, cart, checkout, forms
  assets/icons/             SVG icons for the Icon block
  assets/img/               logo, favicon, social image
  assets/fonts/             Fraunces and Inter, with their licenses
  src/theme.css             the Tailwind CSS source of assets/css/theme.css
  src/icons.css             icons drawn before or after text with CSS (generated)
  src/editor-css.mjs        writes assets/css/editor.css after each build
  languages/                freeframe-market.pot, the template for translations
  blueprint.json            WordPress Playground setup used by `pnpm dev`
  screenshot.png            the picture shown in Appearance > Themes
  package.json              the CSS build and the local WordPress (Node.js tooling)
  readme.txt, README.md, DOCS.md, CHANGELOG.md, LICENSE.txt

Customising#

Brand and colours#

The colours are in theme.json (settings.color.palette) and in Appearance > Editor > Styles > Colors > Palette:

Colour Default Used for
Paper #f6f1ea the page background
Sand, Oat #ebe3d6, #e2d7c6 panels, product image backgrounds, the footer
Line #d9cebd borders
Ink #1f1c19 text, primary buttons, the announcement bar
Stone #645d54 secondary text
Clay, Clay dark #a1441d, #7f3414 the accent: eyebrows, stars, the cart badge, hovers
Moss #56653f the second accent: ticks, the newsletter band, shipping progress
Cream #fbf8f3 fields, cards and text on dark
White, Black for your own blocks

The theme's stylesheet reads these colours from the palette (bg-paper, text-clay...), so changing Clay in Styles recolours every accent at once. The product art draws its own colours: the glaze colours are in the product list and the backdrops in BACKDROPS (assets/js/main.js).

Typography#

Two fonts are bundled in assets/fonts/ and declared in theme.json (settings.typography.fontFamilies), so no request goes to Google: Fraunces (variable, 300 to 600, with italics; preset display) for headings and prices, and Inter (400 to 600; preset sans) for text. To use other fonts, install them in Styles > Typography > Fonts (the Font Library) and select them, or replace the fontFace entries in theme.json. The utility classes read the presets (--wp--preset--font-family--display, --sans), so keep those slugs.

Font sizes are the presets in theme.json (xs 0.75rem to 9xl 8rem, the Tailwind scale).

Content#

Edit pages in Pages: click any text to change it, select a button to change its link. Sections are groups; open List View (the stacked lines icon at the top left) to select a section, move it, duplicate it or delete it. To add a section from another page, use the block inserter (+) > Patterns > Market.

What the store script draws or fills is in Custom HTML blocks: edit those in the block's HTML view, and keep their ids and data- attributes.

Pattern Custom HTML What it is
home-hero, collections, process, about-gallery, cart-empty, 404 <div data-art="..."> product illustrations, see Images and artwork
bestsellers, related the product grid product cards from the product list
shop-catalogue the filters, the result count and sort, the grid, "Nothing on this shelf" see Shop filters
product-breadcrumb, product-details the breadcrumb, the gallery and the buy box filled from the product in the URL
cart-header, cart-full the title's item count, the cart lines, totals, promo code filled from the cart
checkout-done, checkout-main the confirmation details, the checkout form, the order lines and totals see Checkout
faq-hero, faq-list, faq-contact, newsletter-band the FAQ search, "No answers match", the contact and newsletter forms see Forms
team the illustrated portraits illustrations; replace them with Image blocks to show photos
header, footer the menu and cart buttons, the footer newsletter form, the cart drawer and the toast driven by the script

A few blocks carry a class (Block settings > Advanced > Additional CSS class(es)) that the script or stylesheet reads:

  • ff-hidden hides a section until the script decides to show it (the empty and full cart, the checkout states). In the editor these sections are shown with a dashed outline.
  • ffd-faq on each FAQ answer and ffd-faq-group on each FAQ section: the search reads them (blocks can't store data- attributes, so the script turns these classes back into them).
  • reveal fades a block in as it scrolls into view.
  • link-card on a group makes the whole group clickable: the first link inside it stretches over the group (the collection cards).

The product list

Products are the PRODUCTS list at the top of assets/js/main.js. Each product looks like this:

js
{
  id: 'ridge-vase', name: 'Ridge Vase', category: 'Vases', price: 68, shape: 'vase',
  rating: 4.9, reviews: 214, badge: 'Bestseller', featured: 1,
  blurb: 'A tall, generous vase thrown in one pull…',
  glazes: [{ name: 'Oat', color: '#e6d9c2' }, { name: 'Moss', color: '#76855a' }],
  sizes: [{ name: 'Medium · 24cm', delta: 0 }, { name: 'Large · 32cm', delta: 26 }],
  details: ['Stoneware, fired to 1260°C', 'Fully glazed inside — watertight'],
  dims: 'Medium: 24cm tall, 14cm wide. Large: 32cm tall, 18cm wide.'
}
Field Notes
id Unique, used in /product/?id=… and in saved carts. Changing an id drops it from visitors' carts
category One of CATEGORIES; to add a category, add it there and a radio button to the shop filters' Custom HTML
price Base price in dollars; each size adds its delta (which can be negative)
shape vase, bud, mug, cup, tumbler, bowl, plate, pitcher or planter (drives the SVG art)
badge Optional label such as Bestseller, New or Limited
featured Optional; products with it appear in the home page bestsellers, sorted by the number
glazes Colour options; color is the glaze colour in the art
sizes Size options; the one with delta: 0 is preselected

Store settings sit just below the list: FREE_SHIPPING (75), STANDARD_SHIPPING / EXPRESS_SHIPPING (8 / 18), TAX_RATE (0.08), PROMO (WELCOME10, 10%) and MAX_QTY (99, in the cart section). Prices are formatted with Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }) (the money helper). The "Free shipping on orders over $75" announcement bar (header pattern) and the shipping FAQ are plain text, so update them when you change FREE_SHIPPING.

Edit assets/js/main.js in a child theme (copy it there and load it instead), or your products are overwritten when you update the theme. Product names and descriptions are data, not translatable strings.

Images and artwork#

Every product image is drawn by art(product, glazeIndex, view) in assets/js/main.js from the product's shape and glaze colour. Views are 0 front, 1 glaze close-up, 2 on a shelf and 3 a pair. Place art anywhere with a Custom HTML block:

html
<div class="art aspect-square overflow-hidden rounded-[1.75rem]" data-art="terrace-planter" data-glaze="1" data-view="2"></div>

To use photos instead, add an image field to each product and return an <img> from art() (or change productCard() and the product gallery). The logo is assets/img/logo.svg in an Image block next to the Site Title block in the header and footer patterns; the name comes from Settings > General > Site Title. Icons are Icon blocks: select one and pick another from the Market collection or WordPress's own. The social image is assets/img/og-image.png (1200 × 630, source og-image.svg); the favicon assets/img/favicon.svg is used until you set a Site Icon in Settings > General.

The header and footer are template parts (parts/header.html, parts/footer.html) showing the header and footer patterns. Edit them in Appearance > Editor > Patterns > Header / Footer. The header pattern starts with the announcement bar.

The header has two Navigation blocks showing the same menu: the desktop one and the one in the mobile menu. Edit the menu once in Appearance > Editor > Navigation (the setup creates "Main menu"; Vases and Mugs & Cups are custom links to /shop/?category=...). A Navigation block that doesn't point at a menu shows the most recently created one; to pin a menu, select the Navigation block and choose it in the block settings, for both blocks.

The footer's Shop and Help menus and social links are List blocks: click a link to change it, press Enter to add one. The social links point to #; replace them with your profiles.

Features#

Cart and mini cart#

The cart is saved in localStorage and synced across tabs. Every element with data-cart-open (the cart button in the header, "View cart" in the toast) opens the slide-out cart; product cards add with their "Quick add" button. The drawer traps focus, closes with Escape, the close button or the backdrop, and returns focus to where it was. Adding an item bumps the header badge and shows a toast with a "View cart" button.

Each cart line (in the drawer and on the cart page) carries data-line="<id>:<glaze>:<size>". After −, + or Remove, keyboard focus stays on the same button of the same line, and the line is updated in place so the quantity's live region reads out the new number. When a line goes (Remove, or − at quantity 1), focus moves to the same button on the next line, or on the previous line if it was the last; when the cart is then empty, it moves to the empty cart's "Shop" link ("Shop the collection" in the drawer, "Shop all ceramics" on the cart page).

Shop filters#

The shop filters by category (radio buttons, with counts), maximum price and sort (featured, price up/down, rating, name). The result count and heading update live, and the category is kept in the URL (/shop/?category=Vases) so filtered views can be linked, as the header menu does. On small screens the filters fold behind a Filters button.

Product page#

/product/?id=daily-mug shows that product (an unknown id shows the first product). The page title, breadcrumb and text fill from the product. Glaze swatches recolour the gallery; sizes change the price; the quantity stepper is limited to 1–99; four thumbnails switch views. Related products prefer the same category.

Promo code and shipping progress#

On the cart page, WELCOME10 takes 10% off. The free-shipping progress bar appears in the drawer and on the cart page.

Checkout#

The checkout validates every field, formats the card number and expiry as you type, checks the card number with the Luhn algorithm (use 4242 4242 4242 4242) and rejects expired dates. On success it shows a confirmation with a random KC- order number and clears the cart. No payment is processed. Replace the setupValidation(form, …) callback in initCheckout() with your payment provider; the comment there marks the spot. For real payments, use a hosted checkout such as Stripe Checkout, or a shop plugin such as WooCommerce, and don't collect card numbers in your own form.

Typing in the FAQ search filters the answers (Details blocks with ffd-faq), opens matches, hides empty sections and announces the number of answers. Each answer is a Details block: add one by duplicating an answer in List View; its summary layout comes from classes starting with [&>summary]: and its plus sign is drawn by CSS (ff-plus).

Back to top#

A button appears after scrolling about one screen down; it scrolls to the top (instantly with reduced motion), moves keyboard focus to the main content and lifts above the toast. It is created by assets/js/main.js; to remove it, delete the "Back to top" part of that file and the .back-to-top styles in src/theme.css, then run pnpm build.

Block styles#

In the block settings' Styles panel:

Block Styles
Button Fill (default, the ink button), Outline, Clay, Light (on dark), Outline (on dark)

The classes is-px-8, is-py-4 and is-text-base on a Button block make it wider, taller or larger (the home page hero and the cart's Checkout button use them).

Forms#

All forms are validated by setupValidation() in assets/js/main.js: errors appear on blur and submit, the first invalid field is focused, and each error is linked with aria-describedby. Rules come from attributes in the forms' Custom HTML: required, type="email", minlength, data-label (the name used in the message) and data-validate (card, expiry, cvc or postcode).

Form Where Behaviour
Newsletter (data-form="newsletter") every footer, home page band replaced with a thank-you message
Contact (data-form="contact") Help & FAQ page replaced with a thank-you message
Promo (#promo-form) Cart applies WELCOME10
Checkout (#checkout-form) Checkout shows the order confirmation

Nothing is sent. To collect newsletter or contact messages, use a form plugin (replace the form's Custom HTML block with the plugin's block; add the classes field, field-label and btn-primary to its fields, labels and button), or post the form in its callback in assets/js/main.js (in a child theme), for example to Formspree:

js
$$('[data-form="contact"]').forEach(function (form) {
  setupValidation(form, function () {
    fetch('https://formspree.io/f/your-id', {
      method: 'POST', body: new FormData(form), headers: { Accept: 'application/json' }
    }).then(function (res) {
      successSwap(form, res.ok ? '<p>Thank you for writing.</p>' : '<p>Something went wrong. Please email us.</p>');
    });
  });
});

Every field has a name, so the request carries all the values; keep a name on any field you add.

Saved data (localStorage)#

Key Value Used for
kiln-cart JSON list of { id, glaze, size, qty } the cart
kiln-promo "WELCOME10" or null the applied promo code

These are the HTML version's keys. The cart is validated every time it is read: unknown products and bad entries are dropped, glaze and size indexes are range-checked, quantities are clamped to 1–MAX_QTY, duplicate lines are merged, and tampered data is overwritten with the cleaned version. Prices are never stored; totals are always calculated from PRODUCTS. If storage is blocked (some private windows), the store still works, but the cart is empty after a reload.

SEO and sharing#

Page titles come from WordPress: the page title, an em dash and the site name (for example "Our studio — Kiln & Co."); the home page shows the site name and tagline (Settings > General). The product page sets its title from the product ("Daily Mug — Kiln & Co.") once the script runs. The theme doesn't output meta descriptions or Open Graph tags; use an SEO plugin such as Yoast SEO, Rank Math or The SEO Framework, and give it assets/img/og-image.png (or your own 1200 × 630 image) as the default social image. WordPress already generates a sitemap at /wp-sitemap.xml.

Accessibility#

Built in: a skip link to the main content (added by WordPress, styled by the theme), landmarks (header, labelled navigation, main, footer), one h1 per page, visible focus rings, the menu button with aria-expanded, a cart drawer that traps focus and returns it, a labelled cart button with the item count, a live result count on the shop, accessible variant radios and quantity stepper, form errors announced through aria-describedby and aria-invalid, labelled product art, and reduced-motion support for every animation. The demo passes axe (WCAG 2.1 A and AA).

When you edit: keep one Heading 1 per page, give images alt text (or mark them decorative), keep link text meaningful, and label any field you add to a form (<label for="...">).

Building and deploying#

You only need this if you change the theme's files. The stylesheet assets/css/theme.css is compiled with Tailwind CSS from src/theme.css and the classes used in patterns/, parts/, templates/, inc/ and assets/js/:

sh
pnpm install
pnpm build   # compile assets/css/theme.css and assets/css/editor.css
pnpm watch   # recompile assets/css/theme.css on every change (run pnpm build before you commit)

Run pnpm build after adding a utility class to a pattern, template or the script's markup, otherwise the class has no style. Classes you type in the editor's Additional CSS class(es) field are only styled if the stylesheet already contains them.

To deploy, zip the theme folder (named freeframe-market, without node_modules/) and upload it in Appearance > Themes, or copy the folder to wp-content/themes/ on your server.

Translations#

The theme is translation-ready with the text domain freeframe-market. All text in patterns, templates and PHP goes through WordPress's translation functions; the script's strings (cart, filters, checkout messages, form errors) are translated in PHP (freeframe_market_script_strings() in inc/setup.php). Product names and descriptions are data in assets/js/main.js. Create a translation with Loco Translate, or with WP-CLI and Poedit:

sh
wp i18n make-pot wp-content/themes/freeframe-market wp-content/themes/freeframe-market/languages/freeframe-market.pot

Save freeframe-market-de_DE.mo (for example) in wp-content/languages/themes/ or in the theme's languages/ folder. Page content created by Market setup is copied into your pages in the site's language at that moment; translate it in the editor like any other content.

Child themes#

Use a child theme for changes you want to keep when Market is updated (your products in particular). Create wp-content/themes/market-child/style.css:

css
/*
Theme Name: Market Child
Template: freeframe-market
*/

Templates, parts, patterns and theme.json in the child theme override the parent's files of the same name. To use your own copy of assets/js/main.js, dequeue the parent's script (freeframe-market) and enqueue yours with the same dependencies in the child's functions.php. The parent's stylesheet only contains the classes the parent uses: if your child theme uses new utility classes, compile a stylesheet for the child (copy package.json and src/theme.css, point the @source lines at the child's folders) and enqueue it.

Troubleshooting#

  • The pages look unstyled or new classes do nothing. The stylesheet only contains classes it found when it was built. Run pnpm build after editing patterns, then reload.
  • "This block contains unexpected or invalid content". A pattern or page was edited by hand with markup that doesn't match the block. Click Attempt recovery, or re-insert the section from Patterns > Market.
  • Product links go to a page that doesn't exist. The script links to /product/, /shop/, /cart/ and /checkout/. If you renamed those pages, update freeframe_market_page_urls() in inc/setup.php.
  • The cart doesn't keep its items. The browser blocks storage (some private windows), or the product ids changed. See Saved data.
  • Market setup says the pages exist, but they are empty. It never overwrites existing pages. Delete (and empty the trash of) the pages with those slugs, or open each page and insert its page pattern from Patterns > Market pages.
  • The forms don't send anything. They are demo forms; see Forms.
  • The theme can't be installed on an older WordPress. Market uses the Icon block's icon collections from WordPress 7.1; update WordPress first.
  • A page shows the title twice or no title. Pages use a template without a title, because the page patterns start with their own heading. For a plain text page (privacy policy, terms), choose the Page with title template in the page's settings.

License#

MIT, which is compatible with the GPL that WordPress requires (see LICENSE.txt in the theme folder). Free for personal and commercial use. The product art and logo are included under the same license. Fraunces and Inter are licensed under the SIL Open Font License 1.1 (OFL.txt in each folder of assets/fonts/).

Search documentation

Search the guides and every template's documentation