Freeframe

Documentation

Crumb

v1.0.1Updated Oct 16, 2026A charming bakery and café website with click-and-collect preorders, a live bake board and a celebration cake builder.

Overview#

Crumb is a website for a neighbourhood bakery and café with click-and-collect preorders, built as a WordPress block theme. The fictional brand is Bramble & Rye, a bakery on St Benedicts Street in Norwich. The theme has a home page with a live "fresh out of the oven" board, this week's specials, a celebration cake teaser, a café teaser, reviews and an open-now badge with the opening hours; a preorder catalogue with shelf filters, per-day availability and quantity limits; a celebration cake builder with a live drawing and price; a café menu with diet filters; a demo checkout with an order number and a calendar file; an "Our story" page; a visit page with a map, opening hours and a wholesale enquiry form; and a 404 page. A basket drawer with collection day, time slot and an order cut-off is on every 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/crumb.

The pages around the shop (headings, copy, how-preordering-works steps, reviews, the story, suppliers, team, café notes, 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 bakes, bake schedule, opening hours, pickup rules, café menu and cake options are data in assets/js/main.js, and the script draws the bake board, the product cards, the bakes (in SVG), the cake builder, the café menu, the basket and the checkout from it. It is a demo or a prototype; to take real orders, see Checkout and calendar file. 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 crumb.zip from freeframe.dev/templates/crumb (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 Crumb.
  3. Go to Appearance > Crumb setup and click Create the demo pages. This creates the Home, Preorder, Celebration cakes, Café menu, Checkout, Our story and Visit & wholesale 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.

To preview the site at another time of day (the bake board, open-now badges and cut-off all follow the bakery's clock), add ?now= to any page URL, for example /?now=2026-10-17T10:15.

Pages#

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

Page Slug Page pattern Sections (pattern files in patterns/)
Home / page-home.php home-hero (shop window, cut-off), bake-board (live board), how-it-works, specials (product cards), cakes-teaser, cafe-teaser, reviews, visit-us (open-now badge, hours, map)
Preorder /shop/ page-shop.php shop-hero, shop-filters (shelf chips, collection day, "only that day's bakes"), shop-catalogue (cards by shelf and the allergen note)
Celebration cakes /cakes/ page-cakes.php cakes-hero, cake-builder (form, live drawing and price), recent-cakes, cake-faq
Café menu /cafe/ page-cafe.php cafe-hero, cafe-menu (diet filters and the menu), cafe-notes
Checkout /checkout/ page-checkout.php checkout-header, checkout (empty basket, the form, the summary and the confirmation)
Our story /about/ page-about.php about-hero, timeline, sourdough, suppliers, team, waste
Visit & wholesale /visit/ page-visit.php visit-hero, find-us (map, hours, contact), getting-here, wholesale (enquiry form, anchor #wholesale), visit-faq
404 any missing URL templates/404.html 404

The script finds each page's features by the elements it contains (the bake board by #bake-board, the catalogue by #shop-results, the café menu by #cafe-menu, the builder by #cake-form, the checkout by #checkout-form), and links to the preorder, cakes, checkout and home pages by their slugs: if you rename one, change its URL in freeframe_crumb_page_urls() in inc/setup.php too, and the links in the header and footer patterns. Shareable filters work on the WordPress URLs as in the HTML version: /shop/?category=bread, /cafe/?diet=vegan,gf. 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-crumb/
  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, theme colour and favicon, 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 > Crumb 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         settings, bakes, art, basket and every page's features
  assets/icons/             SVG icons for the Icon block
  assets/img/               logo, favicon, social image
  assets/fonts/             Corben, Karla and Delicious Handrawn, 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-crumb.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
Flour #fbf5ec Page background
Paper #fffdf8 Cards and form fields
Oat #f3e7d5 Soft panels, bread tiles
Line #e7d6bf Hairlines and dashed rules
Ink #3a2620 Text, outlines, the footer
Muted #6c574a Secondary text
Bramble, Bramble deep #74305a, #561f42 The brand: buttons, links, the basket
Bramble soft #f3dce7 Selected options
Blush, Blush deep #f7dcd3, #a33a3f Cake tiles, sold out, error text
Pistachio, Pistachio deep #dfe8cb, #43602a Whole cake tiles, "on the shelf", vegan
Butter, Butter deep #f9e8b2, #7a5a06 Pastry tiles, "baking now", badges
Lilac, Sky #e5ddf0, #d9e8ee Pastel accents
Crust #b9733c Decoration only (steam, bread brown)
White, Black For your own blocks

The theme's stylesheet reads these colours from the palette (bg-bramble, text-muted...), so changing Bramble in Styles recolours every brand surface at once. The illustrations use their own colours: the bakes and cakes are drawn by assets/js/main.js (section 6), the shop window, map, coffee cup and other drawings are inline SVG in Custom HTML blocks, so a new brand colour doesn't recolour the bakes. The tile colour behind each shelf's bakes is CATEGORY_TINT in main.js.

Typography#

Three fonts are bundled in assets/fonts/ and declared in theme.json (settings.typography.fontFamilies), so no request goes to Google: Corben (400 and 700; preset display) for headings, prices and the wordmark, Karla (variable, 400 to 800, with italics; preset sans) for body text and UI, and Delicious Handrawn (preset hand, used through the hand class) for the hand-lettered notes and the cake message. The body text is 17px (styles.typography.fontSize). 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, --hand), so keep those slugs. The SVG artwork names 'Delicious Handrawn' and Corben directly for its labels.

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

What the 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
header, footer, visit-us, visit-hero, cafe-hero data-open-status, data-cutoff open-now badges and the cut-off countdown, see Shop clock and opening hours
visit-us, find-us <ul data-hours-list> the week's hours with today highlighted
bake-board <ul id="bake-board"> see Bake board
specials, shop-catalogue #specials, #shop-count, #shop-results product cards drawn from PRODUCTS
shop-filters the shelf chips, the collection-day select and the toggle see Preorder catalogue
cake-builder <form id="cake-form"> see Celebration cake builder
cafe-menu the diet filters, #cafe-count, #cafe-menu, #cafe-empty see Café menu filters
checkout [data-checkout-main] (the form and summary), [data-checkout-done] see Checkout and calendar file
wholesale, footer the wholesale and bake list forms see Forms
page headers, cakes-teaser, cafe-teaser, visit-us, find-us, about-hero, recent-cakes, 404 the illustrations (role="img" with a label) see Images and artwork
how-it-works, team, wholesale, shop-catalogue, checkout the coloured bake drawings and portraits (aria-hidden) decorative illustrations
header, footer the basket and menu buttons, the basket 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 block until the script decides to show it (the empty checkout, the mobile menu). In the editor these are shown with a dashed outline. Classes starting with ffd- become data- attributes when the page loads (ffd-checkout-empty on the empty-basket group, ffd-last-order on the box that shows the last order).
  • 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 logo).
  • card, panel and arch / arch-sm draw the template's cards, soft panels and arched shop windows; grain adds the paper texture; eyebrow the small bramble heading above titles; h-display the Corben heading style; hand the hand-lettered font; squiggle the wavy underline under a word; status-pill with status-fresh, status-baking, status-out or status-sold and ff-dot the bake board's status pills with their dot.

The bakes and shop data

Everything the scripts work from is at the top of assets/js/main.js:

Constant What it holds
SHOP Name, address, phone and email used in the drawer, toasts and calendar file
TIMEZONE, LOCALE, CURRENCY Europe/London, en-GB, GBP
HOURS Opening hours per weekday, Sunday first, ['07:30', '16:00'] or null for closed
PICKUP cutoff ('14:00'), slotStep (30), lastSlotBeforeClose (30), daysAhead (14), notesMax (200)
CATEGORIES The shelves: bread, pastry, bakes, whole
ALLERGENS, DIETS Allergen names and diet labels (vegan, vegetarian, gf, df)
PRODUCTS Every bake on the preorder page
BAKE_SCHEDULE, BAKING_MINS When each bake comes out of the oven, and how long "baking now" shows before that
CAFE_MENU Café sections and items
CAKE Celebration cake options, lead time and limits

Add a bake by adding an object to PRODUCTS:

js
{ id: 'rye-bun', name: 'Caraway rye bun', category: 'bread', price: 1.6, unit: 'each', days: [5, 6], max: 12,
  blurb: 'Soft rye buns with caraway, made for sandwiches.',
  allergens: ['gluten'], diet: ['vegan'], art: { kind: 'boule', crust: '#8c5532', top: '#a8693d' } },
Field Meaning
id Unique, used in the basket and in data-add buttons
category One of the CATEGORIES ids
price In pounds; whole numbers show without pence (£32)
days Weekdays it's baked (0 Sunday to 6 Saturday), or null for every day you're open
max The most one order can hold
leadDays Optional days' notice (default 1: order before the cut-off the day before)
special Optional badge text; specials also appear on the home page
allergens, diet Keys from ALLERGENS and DIETS
art How it's drawn (see Images and artwork)

To show a new bake on the home page board, add it to BAKE_SCHEDULE with the time the first tray comes out and, optionally, when it usually sells out:

js
{ id: 'rye-bun', out: '09:15', soldOut: '13:00' },

Café items are { name, price, desc, diet } inside a CAFE_MENU section. The cake builder reads CAKE.sizes, CAKE.sponges, CAKE.fillings, CAKE.finishes and CAKE.colours; each option has an id, a label and a price (a size has serves, factor and tiers, the diameters in inches used for the drawing).

Prices are never stored in the basket, so changing a price updates every page, saved basket and total. The opening hours in the footer and the café teaser on the home page, the address and phone number in the header, footer and visit page, and the "Order by 2pm" text are also written in the patterns: update them alongside HOURS, PICKUP and SHOP.

Edit assets/js/main.js in a child theme (copy it there and load it instead), or your bakes are overwritten when you update the theme. Bake names, descriptions, café dishes and cake options are data, not translatable strings (shelf names, diets and allergens are translatable).

Images and artwork#

There are no photos. All artwork is SVG:

  • Bakes are drawn by Art.bake(art) in section 6 of main.js from a product's art object. kind picks the drawing: boule, tin, baguette, focaccia, croissant, painchoc, bun, knot, scone, tart, cookie, brownie, loafcake or layercake. Optional colours: crust and top (breads), seeds / oats (tin), score: 'leaf' (boule), almonds (croissant), dough / swirl (bun), sponge, frosting, filling and top ('berries', 'walnuts', 'drip', 'flowers') for layercake.
  • Celebration cakes are drawn by Art.cake(config) from the builder's choices.
  • The hero shop window, the maps, the kitchen window, the coffee cup, the recent cakes and the 404 loaf are inline SVG in Custom HTML blocks: the wrapper carries role="img" and a translatable aria-label. They stay inline (not image files) because their hand-written labels use the theme's fonts and the steam is animated with CSS. To use a photo instead, replace the Custom HTML block with an Image block and give it alt text; keep the hero image around 560 × 660 (portrait).
  • The coloured step drawings, team portraits and the bakes beside the wholesale list are inline SVG marked aria-hidden="true": decorative, replace or delete them freely.

The logo is assets/img/logo.svg in an Image block, next to the "Bramble & Rye" wordmark (a paragraph whose link goes home, with the hand-lettered ampersand) in the header and footer patterns. Icons are Icon blocks: select one and pick another from the Crumb 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 dark bar holding the open-now badge and the cut-off countdown.

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 first link (Home) is hidden in the desktop header, as in the HTML version (the logo links home); keep Home first, or remove the .site-nav … :first-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 mobile menu's Preorder for pickup button and the address are blocks below it.

The footer's Explore links are a List block: click a link to change it, press Enter to add one. The footer hours are paragraphs; the open-now badge below them is filled by the script.

Features#

Shop clock and opening hours#

All times use the bakery's clock (TIMEZONE) via shopNow(), so a visitor abroad sees the shop's real state. Elements with data-open-status show "Open now until 4pm" or "Closed · opens Tuesday 7:30am" with a coloured dot; data-cutoff shows the countdown to the order cut-off ("Order in the next 2h 14m to collect tomorrow"); data-hours-list lists the week with today highlighted. They refresh every 30 seconds. Add ?now=YYYY-MM-DDTHH:MM to a URL to test any time.

Bake board#

The home page's "Fresh out of the oven" list (#bake-board, renderBoard()) shows every BAKE_SCHEDULE entry baked today. Each bake is Out at 7:30am until BAKING_MINS before its time, then Baking now, then Out since 7:30am, and Sold out after its optional soldOut time. Bakes on the shelf come first and sold-out ones last. After closing (and on closed days) it shows the next bake day instead, so the page never looks empty. It refreshes every minute. Each card links to its shelf on the preorder page.

Preorder catalogue#

initShop() renders PRODUCTS grouped by shelf. The chips filter by shelf and update the URL (?category=pastry); the collection-day picker is the basket's day; "Only that day's bakes" hides anything not baked then. Cards show the bake days as a Monday-first week strip, diet tags, allergens and an action:

  • Add to basket when it fits the chosen day.
  • Add for Sat 17 Oct when it doesn't: adding it moves the collection to the next day that suits the whole basket, and a toast says so.
  • A quantity stepper once it's in the basket, capped at max.

Filters are buttons with aria-pressed, and the result count is a live region. The home page's specials use the same cards.

Basket drawer#

The basket button in the header opens #basket-drawer, a modal dialog with a focus trap; Esc, the close button or the backdrop close it and focus returns to the button. It lists each line with a stepper and remove button, then the collection details:

  • Collection day: open days from the earliest possible (earliestDate()) up to PICKUP.daysAhead (CAKE.daysAhead when there's a celebration cake). Orders before PICKUP.cutoff can collect the next open day; after it, the day after. Whole cakes add their leadDays, celebration cakes CAKE.leadDays.
  • Time slot: half-hour slots from opening time until lastSlotBeforeClose minutes before closing.
  • Conflicts: if something isn't baked on the chosen day, the drawer says so, offers the next day that suits everything, and disables checkout until it's fixed.
  • Notes up to PICKUP.notesMax characters, saved as you type.

Celebration cake builder#

initCakes() builds the radio cards from CAKE and redraws the cake and price on every change. The price is the size's price plus sponge, filling and finish extras multiplied by the size's factor, rounded to whole pounds (cakePrice()). The colour swatches are disabled for the naked finish. The message is limited to CAKE.messageMax characters with a live counter. The date input's minimum is CAKE.leadDays days ahead (after the cut-off) and closed days are rejected. Adding the cake puts it in the basket, sets the collection to that date and shows a toast; one order holds up to CAKE.maxPerOrder cakes. The builder is one Custom HTML block (cake-builder): its option cards are drawn by the script, so edit the options in CAKE, not in the block.

Café menu filters#

initCafe() renders CAFE_MENU. The diet chips combine (a dish must match all of them; vegan counts as vegetarian and dairy-free), update the URL (?diet=vegan,gf) and show an empty state with a clear button.

Checkout and calendar file#

initCheckout() shows the collection picker, contact fields, payment choice and summary. "Pay now by card" reveals a demo card form (Luhn-checked number, MM / YY expiry, CVC). On a valid submit it creates an order number (BR- and five digits), saves the order to crumb-last-order, empties the basket and shows a confirmation with an Add to calendar (.ics) link built by icsFor(): a 30-minute event at the collection slot in Europe/London, with the items in the description. The empty checkout shows the last order and its calendar link.

Nothing is sent and no payment is taken. For real orders, send the order object in the checkout's submit handler in main.js (a comment marks the spot) to your order system, and take payment with your provider (Stripe Checkout, Square, SumUp…) before showing the confirmation; remove the demo card form from the checkout pattern when you do.

Scroll reveal, motion and back to top#

Blocks with reveal fade in as they scroll into view. The steam over the shop window and coffee cup rises, bakes lift on hover and the "Baking now" dot glows. All motion is off for visitors who prefer reduced motion. A back-to-top button appears after one screen of scrolling; it lifts above the toast and hides while it would cover the sticky cake preview or order summary (data-back-to-top-hide). It is created at the end of 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 bramble button), Outline, Soft, Ink

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

FAQ answers on the cakes and visit pages are Details blocks: add one by duplicating an answer in List View; its summary layout comes from classes starting with [&>summary]:, and the round plus sign is a span at the end of the summary text that turns into a cross when it opens.

Forms#

All forms validate in the browser with inline messages (fieldMessage() in assets/js/main.js): errors appear under each field on blur and submit, the first invalid field gets focus, and each error is linked with aria-describedby. Rules come from attributes in the forms' Custom HTML: required, type="email", type="tel", type="number" with min and max, type="date" with data-min-days, maxlength, data-label (the name used in the message), data-validate (name, card, expiry, cvc, open-day) and, for a group of checkboxes, data-group with data-msg.

Form Where Validates
Bake list sign-up (data-form="newsletter") Footer, every page Email
Wholesale enquiry (data-form="wholesale") Visit & wholesale Business, type, name, email, optional phone, loaves a day (10–400), at least one interest, start date at least 7 days ahead, message up to 600 characters
Cake builder (#cake-form) Celebration cakes Collection date (lead time, open day), message length
Checkout (#checkout-form) Checkout Time slot, name, email, UK phone, allergen confirmation, card fields when paying now

The demo forms send nothing until you connect them. 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 initForms() in assets/js/main.js (in a child theme), for example to Formspree:

js
$$('[data-form="wholesale"]').forEach((form) => setupValidation(form, () => {
  fetch('https://formspree.io/f/your-id', { method: 'POST', body: new FormData(form), headers: { Accept: 'application/json' } })
    .then((r) => r.ok ? successSwap(form, '<p class="font-display text-2xl">Thanks, we’ll be in touch.</p>') : alert('Sorry, that didn’t send.'));
}));

Every field already has a name; keep a name on any field you add.

Saved data (localStorage)#

Key Shape Notes
crumb-basket { lines, date, time, notes } lines are { id, qty } or { id: 'cake', key, cake: { size, sponge, filling, finish, colour, message }, qty: 1 }; date is YYYY-MM-DD, time is HH:MM
crumb-last-order { number, placedAt, date, time, notes, name, email, phone, payment, lines, total } Written at checkout; shown on the empty checkout page

These are the HTML version's keys. Everything is checked on every read (loadBasket(), readLastOrder()): unknown products and cake options are dropped, quantities become whole numbers within max, duplicates merge, dates outside the bookable range and slots that don't exist reset, and the repaired value is written back. Prices are never stored; they always come from PRODUCTS and CAKE. Open tabs stay in sync through the storage event. To reset, run localStorage.removeItem('crumb-basket') in the console, or clear the site's data. Change the key names if you run several sites on one domain.

SEO and sharing#

Page titles come from WordPress: the page title, an em dash and the site name (for example "Preorder — Bramble & Rye"); the home page shows the site name and tagline (Settings > General). 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 (consider leaving out the checkout page). A Bakery schema.org block (name, address, opening hours, telephone) helps local search.

The bake board, product cards and café menu are rendered by JavaScript from the data in main.js. Search engines that run JavaScript (such as Google) see them; if you need them in the page's HTML, add them as blocks.

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 outlines in the brand colour (butter on the dark bands), the menu button with aria-expanded that closes with Esc, a basket drawer (role="dialog", aria-modal) with a focus trap that returns focus to its opener, filter chips as toggle buttons (aria-pressed), live regions for result counts, the cake price and toasts, labelled steppers ("One more Butter croissant"), illustrations with text alternatives or hidden from screen readers, form errors announced through aria-describedby and aria-invalid, 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. Tailwind finds classes by scanning the files, so classes used in main.js must be written out in full there.

To deploy, zip the theme folder (named freeframe-crumb, 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-crumb. All text in patterns, templates and PHP goes through WordPress's translation functions, including the illustrations' labels; the script's strings (opening times, the bake board, cards, basket, cake builder, checkout and form messages, shelf, diet and allergen names) are translated in PHP (freeframe_crumb_script_strings() in inc/setup.php). Bakes, café dishes and cake options 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-crumb wp-content/themes/freeframe-crumb/languages/freeframe-crumb.pot

Save freeframe-crumb-fr_FR.mo (for example) in wp-content/languages/themes/ or in the theme's languages/ folder. Page content created by Crumb setup is copied into your pages in the site's language at that moment; translate it in the editor like any other content. Prices and dates are formatted for LOCALE (en-GB) and CURRENCY (GBP) in main.js.

Child themes#

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

css
/*
Theme Name: Crumb Child
Template: freeframe-crumb
*/

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-crumb) 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 > Crumb.
  • The bake board says "Tomorrow" all day. HOURS decides when you're open; check the closing time for today and that TIMEZONE is your bakery's zone.
  • A bake never appears on the board. It needs a BAKE_SCHEDULE entry and must be baked that day (days).
  • Customers can't pick tomorrow. It's after PICKUP.cutoff, tomorrow is closed, or something in the basket needs more notice (leadDays).
  • Links in the basket go to a page that doesn't exist. The script links to /shop/, /cakes/, /checkout/ and the front page. If you renamed those pages, update freeframe_crumb_page_urls() in inc/setup.php.
  • Crumb 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 > Crumb pages.
  • The calendar file has the wrong time. Events use TIMEZONE; if you change it, update the VTIMEZONE rules in icsFor() too.

License#

MIT, which is compatible with the GPL that WordPress requires (see LICENSE.txt in the theme folder). Free for personal and commercial use. All artwork (bakes, cakes, maps, logo and illustrations) is included under the same license. Corben, Karla and Delicious Handrawn 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