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.
- Download
market.zipfrom freeframe.dev/templates/market (the WordPress tab). - 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.
- 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.
- 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:
pnpm install
pnpm dev # builds the CSS, then starts WordPress with the theme at http://127.0.0.1:9400pnpm 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.txtCustomising#
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-hiddenhides 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-faqon each FAQ answer andffd-faq-groupon each FAQ section: the search reads them (blocks can't storedata-attributes, so the script turns these classes back into them).revealfades a block in as it scrolls into view.link-cardon 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:
{
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:
<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.
Navigation, header and footer#
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.
FAQ search#
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:
$$('[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/:
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:
wp i18n make-pot wp-content/themes/freeframe-market wp-content/themes/freeframe-market/languages/freeframe-market.potSave 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:
/*
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 buildafter 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, updatefreeframe_market_page_urls()ininc/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/).