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.
- Download
crumb.zipfrom freeframe.dev/templates/crumb (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 Crumb.
- 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.
- 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.
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.txtCustomising#
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-hiddenhides 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 withffd-becomedata-attributes when the page loads (ffd-checkout-emptyon the empty-basket group,ffd-last-orderon the box that shows the last order).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 logo).card,panelandarch/arch-smdraw the template's cards, soft panels and arched shop windows;grainadds the paper texture;eyebrowthe small bramble heading above titles;h-displaythe Corben heading style;handthe hand-lettered font;squigglethe wavy underline under a word;status-pillwithstatus-fresh,status-baking,status-outorstatus-soldandff-dotthe 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:
{ 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:
{ 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 ofmain.jsfrom a product'sartobject.kindpicks the drawing:boule,tin,baguette,focaccia,croissant,painchoc,bun,knot,scone,tart,cookie,brownie,loafcakeorlayercake. Optional colours:crustandtop(breads),seeds/oats(tin),score: 'leaf'(boule),almonds(croissant),dough/swirl(bun),sponge,frosting,fillingandtop('berries','walnuts','drip','flowers') forlayercake. - 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 translatablearia-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.
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 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 toPICKUP.daysAhead(CAKE.daysAheadwhen there's a celebration cake). Orders beforePICKUP.cutoffcan collect the next open day; after it, the day after. Whole cakes add theirleadDays, celebration cakesCAKE.leadDays. - Time slot: half-hour slots from opening time until
lastSlotBeforeCloseminutes 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.notesMaxcharacters, 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 | |
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:
$$('[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/:
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:
wp i18n make-pot wp-content/themes/freeframe-crumb wp-content/themes/freeframe-crumb/languages/freeframe-crumb.potSave 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:
/*
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 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 > Crumb.
- The bake board says "Tomorrow" all day.
HOURSdecides when you're open; check the closing time for today and thatTIMEZONEis your bakery's zone. - A bake never appears on the board. It needs a
BAKE_SCHEDULEentry 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, updatefreeframe_crumb_page_urls()ininc/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 theVTIMEZONErules inicsFor()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/).