Freeframe

Documentation

Roast

v1.0.1Updated Oct 16, 2026A bold shop for a specialty coffee roaster, with a subscription builder, brew guides and a working cart.

Overview#

Roast is a shop for a specialty coffee roaster, built as a WordPress block theme. The fictional brand is First Crack, a roaster in an old tram depot in Baltimore, Maryland. The theme has a home page with this week's roasts, a roast finder, a subscription teaser, brew guides and reviews; a shop with roast, origin and process filters; product pages with bag sizes, grinds, a tasting profile and a brew recipe; a subscription builder that shows the price per cup and the next delivery dates; a cart drawer; a three-step demo checkout; brew guides with a recipe scaler; a wholesale page with a volume estimator and an enquiry form; a sourcing page; a contact page with an FAQ; 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/roast.

The pages around the shop (headings, copy, roast cards, reviews, the sourcing and contact pages, buttons) are core WordPress blocks arranged in block patterns, so you edit them in the block editor without touching code. The shop itself works as in the HTML version: the coffees are one JavaScript list in assets/js/main.js, and the script draws the coffee cards, the product page, the cart, the subscription prices 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 roast.zip from freeframe.dev/templates/roast (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 Roast.
  3. Go to Appearance > Roast setup and click Create the demo pages. This creates the Home, Shop coffee, Product, Subscribe, Checkout, Brew guides, Wholesale, Our sourcing and Contact pages from the theme's page patterns, sets Home as the front page (Settings > Reading) and creates the header menu. 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 > Roast pages) made of section patterns. Every section is also offered on its own in the block inserter under Patterns > Roast.

Page Slug Page pattern Sections (pattern files in patterns/)
Home / page-home.php home-hero (with bag art), ticker, featured-roasts (coffee cards), find-your-roast, subscription-teaser, brew-teaser, sourcing-stats, reviews
Shop coffee /shop/ page-shop.php shop-hero, shop-catalogue (filters, sort and coffee grid), shop-promises
Product /product/?id=kochere page-product.php product-breadcrumb, product-details (gallery, size, grind, add to cart), product-info (tasting profile, origin, brew recipe), related
Subscribe /subscribe/ page-subscribe.php subscribe-hero, plan-builder (the five steps and the plan summary), subscribe-start (sign-up form)
Checkout /checkout/ page-checkout.php checkout-header, checkout-done (confirmation), checkout-empty, checkout-main (steps and order summary)
Brew guides /brew/ page-brew.php brew-hero, brew-guides (method tabs and recipe scaler), brew-cta
Wholesale /wholesale/ page-wholesale.php wholesale-hero, wholesale-benefits, wholesale-pricing (price table and estimator), wholesale-enquiry
Our sourcing /about/ page-about.php about-hero, how-we-buy, prices-paid, producers, visit-cta
Contact /contact/ page-contact.php contact-hero, contact-form (form, roastery hours, other contacts), faq
404 any missing URL templates/404.html 404

The script finds the shop, product, subscription, checkout and brew guide pages by their slugs (shop, product, subscribe, checkout, brew): if you rename one, change its URL in freeframe_roast_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-roast/
  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 > Roast 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 shop: coffees, bag art, cart, subscription, checkout, brew guides, forms
  assets/icons/             SVG icons for the Icon block
  assets/img/               logo, favicon, social image
  assets/fonts/             Unbounded and Onest, 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-roast.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
Butter #fff6df the page background, text on dark
Cream #fffcf4 cards and fields
Oat #f6e9c8 soft panels, page headers
Line #e3d5b3 hairlines
Sun #ffcd4a stickers, highlight bands, the second button style
Cobalt, Cobalt dark #2338b5, #18268a the brand: hero bands, primary buttons, links
Sky #d9e0ff selected options, info panels
Navy #141a3c text, outlines and the offset "sticker" shadows, the footer
Muted #4b4f6e secondary text
Tomato, Tomato dark #e0522f, #b03417 decoration; the dark shade for text (stars, errors, the cart badge)
Leaf #23714a success messages and savings
White, Black for your own blocks

The theme's stylesheet reads these colours from the palette (bg-cobalt, text-muted...), so changing Cobalt in Styles recolours every brand surface at once. The bag artwork draws its own colours: each coffee's bag colour is in the coffee list (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: Unbounded (variable, 500 to 800; preset display) for headings, prices and labels, and Onest (variable, 400 to 700; 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. The bag labels in the SVG art name the fonts directly (font-family="Unbounded, …" in art()).

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 > Roast.

What the shop 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 the two linked bags (<a data-art="kochere">) bag illustrations, see Images and artwork
ticker the scrolling tasting notes the second copy is aria-hidden so screen readers read it once
featured-roasts, related the coffee grid coffee cards from the coffee list
shop-catalogue the filters, the result bar, the grid, the empty state see Shop filters
product-breadcrumb, product-details, product-info the breadcrumb, the gallery and buy box, the three info cards filled from the coffee in the URL
plan-builder the five steps and the plan summary see Subscription builder
checkout-done, checkout-main the confirmation details, the checkout steps, the order lines and totals see Checkout
brew-guides the method tabs, their panels and the recipe scaler see Brew guides
wholesale-pricing the price table and the estimator see Wholesale estimator
about-hero the globe illustration decorative SVG
subscribe-start, wholesale-enquiry, contact-form the forms see Forms
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 checkout confirmation and empty-cart states, the mobile menu). In the editor these sections are shown with a dashed outline.
  • 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 roast cards on the home page, the logo).
  • card draws the template's outlined card with its offset shadow; sticker the yellow label; eyebrow the small blue heading above titles.

The coffee list

Coffees are the COFFEES list at the top of assets/js/main.js. Each coffee looks like this:

js
{
  id: 'kochere', name: 'Kochere Lot 7', origin: 'ethiopia', region: 'Yirgacheffe, Gedeo zone',
  process: 'washed', roast: 'light', price: 21, rating: 4.9, reviews: 312, badge: 'New harvest', featured: 1,
  notes: ['Jasmine', 'Bergamot', 'White peach'], profile: { acidity: 5, sweetness: 4, body: 2 },
  producer: 'Worka Sakaro washing station', altitude: '1,950–2,150 m', variety: 'Ethiopian landraces', harvest: 'December 2025',
  bag: '#f4a6c4', brew: 'v60',
  blurb: 'Tea-like and floral, with a long peach finish…'
}
Field Notes
id Unique, used in /product/?id=… (any letter case) and in saved carts. Changing an id drops it from visitors' carts
origin, roast, process Keys of ORIGINS, ROASTS and PROCESSES; to add an origin, add it there and a checkbox to the shop filters' Custom HTML
price The 250 g price in dollars. The other bag sizes are price × factor from SIZES (500 g × 1.85, 1 kg × 3.4), rounded to whole dollars
profile Acidity, sweetness and body from 1 to 5 (the tasting profile bars)
bag The bag colour in the artwork
brew A key of METHODS (v60, aeropress, french, moka, cold, espresso): the recipe shown on the product page
badge Optional label such as Bestseller or Limited
featured Optional; coffees with it appear in "This week's roasts", sorted by the number

Shop settings sit just below the list: SIZES, GRINDS, FREE_SHIPPING (45), SHIPPING (standard $6, express $12, pick-up free), MAX_QTY (20), the subscription settings (STYLES, SUB_DISCOUNT 10%, DOSE 15 g a cup, FREQUENCIES, MAX_BAGS, ROAST_DAY, DELIVERY_LAG) and the brew METHODS. Prices are formatted with Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }). The "Free shipping over $45" announcement bar (header pattern), the shop and FAQ copy and the delivery options' descriptions are plain text, so update them when you change the settings; the prices themselves are always worked out by the script.

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

Images and artwork#

Every coffee picture is drawn by art(coffee, view) in assets/js/main.js: view 0 is the bag (its colour, a label with the origin, process and a roast mark: a sun, waves or a moon) and view 1 the roasted beans. Place art anywhere with a Custom HTML block:

html
<div class="art aspect-[4/5] overflow-hidden rounded-3xl border-2 border-navy" data-art="gatomboya" data-view="1"></div>

Add data-decorative to hide it from screen readers. To use photos instead, add an image field to each coffee and return an <img> from art(). 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 Roast 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"). Its last link (Contact) is hidden in the desktop header, which has its own Contact link; keep Contact last, or remove the .site-nav … :last-child rule in src/theme.css. 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 Coffee 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 drawer#

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 cart drawer; coffee cards add a 250 g whole-bean bag with their "Add 250 g" button. The drawer lists each line with its size, grind and unit price, a quantity stepper (1 to MAX_QTY) and Remove, shows the free-shipping progress and the subtotal, traps focus, closes with Escape, the close button or the backdrop, and returns focus to where it was. Without JavaScript the cart button goes to the checkout page.

Shop filters#

The shop filters by roast, origin and process (checkboxes with counts; any within a group, all groups together) and sorts by featured, price, roast or name. The count is announced, active filters show as removable chips, and the state is kept in the URL (/shop/?roast=light,medium&origin=ethiopia&sort=price-asc) so filtered views can be linked, as the footer does. Parameter names and values are read in any letter case and anything that isn't one of the shop's own values is ignored. On small screens the filters fold behind a Filters button.

Product page#

/product/?id=gatomboya shows that coffee (an unknown or missing id shows the first one). The page title, breadcrumb, tasting notes, profile bars, origin details and brew recipe fill from the coffee. Bag sizes show their prices, the grind is a select, the quantity stepper is limited to 1–20, and the add button shows the line total. Two thumbnails switch between the bag and the beans. "Build a plan" links to the subscription builder with the matching style. Related coffees prefer the same roast.

Subscription builder#

The builder has five steps: style (STYLES), bag size, bags per delivery (1 to MAX_BAGS), how often (FREQUENCIES, in weeks) and grind. The summary shows each choice with a Change button (which scrolls to and focuses that step), the price per delivery and per cup (DOSE grams a cup), a monthly estimate, the saving against shop prices (SUB_DISCOUNT) and the next three delivery dates: the first is DELIVERY_LAG days after the next ROAST_DAY (Monday) after today, the rest follow every few weeks. Dates are written in plain text, so they read the same on any day. The plan is saved and restored on the next visit; /subscribe/?style=decaf preselects a style. The "Start subscription" form is a demo (see Forms).

Checkout#

The checkout has three steps (details, delivery, payment) with a step indicator. Each step is validated before the next; the delivery step offers standard (free over FREE_SHIPPING), express and roastery pick-up with prices from SHIPPING; the payment step shows a review with Edit links and formats the card number and expiry as you type. On success it shows a confirmation with a random FC- order number and clears the cart. No payment is processed. The submit handler in initCheckout() marks the spot for your payment provider. 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.

Brew guides#

The brew page has one tab per method (role="tablist"; arrow keys, Home and End move between tabs). /brew/#aeropress opens a tab directly (any letter case). The recipe scaler reads the open method's ratio and cup size from METHODS: choose the number of cups and the strength (one point more or less water per gram) and it shows the coffee, water and ratio. To add a method, add it to METHODS, a tab and a panel in the Custom HTML of brew-guides.

Wholesale estimator#

On the wholesale page, drinks a day and days open each week give the kilos of coffee a week (WHOLESALE_DOSE, 18 g a drink), the matching price tier from TIERS (highlighted in the table) and an estimated weekly spend. The table itself is text in the wholesale-pricing pattern: keep it in step with TIERS.

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 cobalt button), Outline, Sun, Light (on dark)

The classes is-px-7, is-py-3-5, is-text-base and is-bg-cream on a Button block make it wider, taller, larger or give an outline button a cream fill (the hero buttons use them).

FAQ answers on the contact page are Details blocks: 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).

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", type="tel", minlength, data-label (the name used in the message), data-validate (zip, order, card, expiry or cvc) and, for a group of checkboxes, data-group with data-msg.

Form Where Behaviour
Newsletter (data-form="newsletter") every footer replaced with a thank-you message
Subscription (data-form="subscribe") Subscribe replaced with a summary of the plan and the first delivery date
Wholesale (data-form="wholesale") Wholesale needs at least one interest; replaced with a thank-you message
Contact (data-form="contact") Contact the order number is optional (FC-12345); replaced with a thank-you message
Checkout (#checkout-form) Checkout shows the order confirmation

Nothing is sent. To collect 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((form) => setupValidation(form, () => {
  fetch('https://formspree.io/f/your-id', {
    method: 'POST', body: new FormData(form), headers: { Accept: 'application/json' }
  }).then((res) => {
    successSwap(form, res.ok ? '<p>Thanks 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
firstcrack-cart JSON list of { id, size, grind, qty } the cart
firstcrack-plan JSON { style, size, bags, freq, grind } the subscription builder

These are the HTML version's keys. Both are validated every time they are read: unknown coffees, sizes, grinds and styles are dropped or replaced with defaults (ids in any letter case are accepted), quantities are clamped to whole numbers from 1 to MAX_QTY, duplicate cart lines are merged, and tampered data is overwritten with the cleaned version. Prices are never stored; totals are always calculated from COFFEES and STYLES. If storage is blocked (some private windows), the shop still works, but the cart and plan are empty after a reload.

SEO and sharing#

Page titles come from WordPress: the page title, an em dash and the site name (for example "Wholesale — First Crack"); the home page shows the site name and tagline (Settings > General). The product page sets its title from the coffee ("Kochere Lot 7 — Ethiopia coffee | First Crack Coffee") 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 and a label that changes with its state, a cart drawer that traps focus and returns it, a labelled cart button with the item count, live result counts and plan prices, radio-card options with visible focus, ARIA tabs on the brew page, form errors announced through aria-describedby and aria-invalid, labelled bag art, and reduced-motion support for every animation (the ticker stops). 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-roast, 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-roast. All text in patterns, templates and PHP goes through WordPress's translation functions; the script's strings (cart, filters, subscription summary, checkout messages, form errors) are translated in PHP (freeframe_roast_script_strings() in inc/setup.php). Coffee 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-roast wp-content/themes/freeframe-roast/languages/freeframe-roast.pot

Save freeframe-roast-de_DE.mo (for example) in wp-content/languages/themes/ or in the theme's languages/ folder. Page content created by Roast 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 Roast is updated (your coffees in particular). Create wp-content/themes/roast-child/style.css:

css
/*
Theme Name: Roast Child
Template: freeframe-roast
*/

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-roast) 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 > Roast.
  • Coffee links go to a page that doesn't exist. The script links to /product/, /shop/, /subscribe/, /checkout/ and /brew/. If you renamed those pages, update freeframe_roast_page_urls() in inc/setup.php.
  • The cart or plan doesn't keep its contents. The browser blocks storage (some private windows), or the coffee ids changed. See Saved data.
  • Contact is missing from the desktop menu, or another link is. The desktop header hides the menu's last link. See Navigation, header and footer.
  • Roast 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 > Roast pages.
  • The forms don't send anything. They are demo forms; see Forms.
  • 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 coffee artwork and logo are included under the same license. Unbounded and Onest 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