Overview#
Slice is a takeaway and delivery site for a pizzeria, built as a WordPress block theme. The
fictional brand is Crust Club, a sourdough pizzeria with two shops in Manchester (Ancoats and
Chorlton). The theme has a home page with a postcode delivery checker, bestsellers, deals, reviews
and both shops' opening status; a menu with diet filters, a sticky section nav and a pizza
customiser (sizes, crusts, extras, half and half); a deals page with set deals and a bundle builder;
a checkout with delivery or pickup, time slots from the opening hours and an animated order
tracker; a locations page with a delivery-zone map; a catering page with a pizza calculator and an
enquiry form; an "Our story" page; and a 404 page. A cart drawer with promo codes and delivery
minimums 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/slice.
The pages around the ordering (headings, copy, how-it-works steps, reviews, the shops, the story,
team and catering packages, buttons) are core WordPress blocks arranged in block patterns, so you
edit them in the block editor without touching code. The ordering itself works as in the HTML
version: the menu, shops, delivery zones, deals and promo codes are data in assets/js/main.js, and
the script draws the menu cards, the pizzas (in SVG), the delivery checker, the cart, the checkout
and the tracker from it. It is a demo or a prototype; to take real orders, see
Checkout and time slots. 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
slice.zipfrom freeframe.dev/templates/slice (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 Slice.
- Go to Appearance > Slice setup and click Create the demo pages. This creates the Home, Menu, Deals & bundles, Checkout, Locations & delivery, Our story and Catering & party orders 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.
Pages#
Each page is a page pattern (in Patterns > Slice pages) made of section patterns. Every section is also offered on its own in the block inserter under Patterns > Slice.
| Page | Slug | Page pattern | Sections (pattern files in patterns/) |
|---|---|---|---|
| Home | / |
page-home.php |
home-hero (delivery checker), bestsellers, home-deals, how-it-works, dough, reviews, shops (open-now badges), home-cta |
| Menu | /menu/ |
page-menu.php |
menu-hero, menu-bar (section links and diet filters), menu-list (menu cards and allergens) |
| Deals & bundles | /deals/ |
page-deals.php |
deals-hero, set-deals, bundle (bundle builder), promo-codes |
| Checkout | /checkout/ |
page-checkout.php |
checkout-header, checkout (empty box, the form, the order summary and the confirmation with the tracker) |
| Locations & delivery | /locations/ |
page-locations.php |
locations-hero, delivery-zones (map, postcode checker and zones), shop-details (hours), delivery-faq |
| Our story | /about/ |
page-about.php |
about-hero, story, dough-steps, sourcing, team, values |
| Catering & party orders | /catering/ |
page-catering.php |
catering-hero, packages, pizza-calculator, enquiry (form), catering-faq |
| 404 | any missing URL | templates/404.html |
404 |
The script finds each page's features by the elements it contains (the menu by #menu-sections,
the checkout by #checkout-form...), and links to the menu, deals and checkout pages by their
slugs (menu, deals, checkout): if you rename one, change its URL in
freeframe_slice_page_urls() in inc/setup.php too, and the links in the header and footer
patterns. 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-slice/
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 > Slice 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 ordering: shops, zones, menu, pizza art, cart, checkout, tracker, forms
assets/icons/ SVG icons for the Icon block
assets/img/ logos, hero pizza, favicon, social image, illustrations
assets/fonts/ Rubik and Shrikhand, 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-slice.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 |
|---|---|---|
| Mozzarella | #fff4e2 |
the page background, text on red and dark |
| Paper | #fffaf2 |
cards and fields |
| Flour | #f9e6c8 |
soft panels, steppers |
| Line | #ecd3ad |
hairlines |
| Tomato, Tomato deep, Tomato soft | #cc2a16, #94190b, #ffd9cc |
the brand: page headers, primary buttons; the deep shade for red text and button lips |
| Cheese, Cheese soft | #ffc23a, #ffe7a6 |
deals, stickers, the second button style, highlights |
| Basil, Basil deep, Basil soft | #1d6b3c, #134d2a, #dbeed3 |
vegan tags, success, savings, dark bands |
| Crust | #e9a35a |
decoration |
| Ink | #2a1209 |
text, the footer, dark buttons |
| Cocoa | #6b4a3a |
secondary text |
| White, Black | for your own blocks |
The theme's stylesheet reads these colours from the palette (bg-tomato, text-cocoa...), so
changing Tomato in Styles recolours every brand surface at once. The pizzas and food drawings use
their own colours, set in the artwork code in assets/js/main.js; the logos are
assets/img/logo.svg and assets/img/logo-footer.svg.
Typography#
Two fonts are bundled in assets/fonts/ and declared in theme.json
(settings.typography.fontFamilies), so no request goes to Google: Shrikhand (one weight;
preset display, used through the h-display class) for big headings and the site title, and
Rubik (variable, 400 to 900, with italics; preset sans) for everything else. 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. Shrikhand only has one
weight, so keep it for large text.
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 > Slice.
What the ordering 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, delivery-zones |
<div data-zone-checker></div> |
the delivery checker, see Delivery checker |
home-hero, page headers |
the hero pizza with its floating ingredients, the decorative pizzas (data-pizza) |
illustrations, see Images and artwork |
bestsellers, home-deals, set-deals |
#bestsellers, #home-deals, #deal-cards |
cards drawn from MENU and DEALS |
shops, shop-details, footer |
data-store-status, "Today: …", the hours tables |
open-now badges and hours, see Open now and opening hours |
menu-hero |
"Delivery or pickup? Change" | the current order mode, opens the drawer |
menu-bar, menu-list |
the section links and diet filters, the count, #menu-sections, the empty state |
see Menu filters and section nav |
bundle |
#bundle-builder |
see Deals and bundles |
promo-codes |
the codes with their Copy buttons | see Promo codes |
checkout |
#checkout-form, #checkout-done |
see Checkout and time slots |
delivery-zones |
the map, the zones table | see Locations map |
pizza-calculator, enquiry |
the calculator, the enquiry form | see Catering calculator and Forms |
how-it-works, dough-steps, sourcing, team |
the coloured SVG drawings | illustrations |
header, footer |
the order summary, order and menu buttons, the newsletter form, the cart drawer and the toast | driven by the script |
A few blocks carry a class (Block settings > Advanced > Additional CSS class(es)) that the script or stylesheet reads:
ff-hiddenhides a block until the script decides to show it (the empty checkout, the mobile menu). In the editor these are shown with a dashed outline.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).carddraws the template's rounded card;eyebrowthe small red heading above titles;h-displaythe Shrikhand heading style;on-redandon-darkswitch focus rings to yellow.scallop-bcuts the scalloped bottom edge of the page headers;checkerdraws the checkered band above the footer;ff-checker-bandsdraws it along the top and bottom of a box (the home page's "Hungry yet?").
The menu and shop data
Everything you can order lives in data at the top of assets/js/main.js:
| Constant | What it holds |
|---|---|
STORES |
Shops: id, name, address, phone, hours (Sunday first, ['12:00', '22:00'] or null), pickupMins, deliveryMins |
ZONES |
Delivery zones: postcode districts, the store that serves them, fee, min order and eta range in minutes |
FREE_DELIVERY_OVER |
Subtotal (after promo) that makes delivery free (35) |
PROMOS |
Demo promo codes (see Promo codes) |
SECTIONS |
Menu sections in order, with the blurb under each heading |
SIZES |
Pizza sizes: delta from the Regular price and extra (price of each extra topping) |
CRUSTS |
Crusts: price, optional sizes they're limited to, allergens, notVegan, glutenFree |
TOPPINGS |
Extra toppings: kind (veg, dairy, honey, meat, fish) decides vegan / vegetarian tags |
MENU |
Every item: section, name, price, desc, diet, allergens, art, optional best |
DEALS |
Set deals with their slots |
BUNDLE_TIERS |
Bundle discount tiers |
Other settings: TIMEZONE ('Europe/London'), LOCALE and CURRENCY (prices are formatted with
Intl.NumberFormat), SLOT_STEP (minutes between time slots), SCHEDULE_DAYS, MAX_QTY and
TRACKER_STEP_SECONDS.
To add a pizza, add an entry to MENU. Pizza prices are the Regular (12″) price; the other sizes
follow SIZES. The picture is drawn from art:
{ id: 'truffle', section: 'pizzas', name: 'Truffle Shuffle', price: 14,
desc: 'Garlic cream, mushrooms, fior di latte and truffle oil.',
diet: ['vegetarian', 'gf'], allergens: ['gluten', 'milk'],
art: { sauce: 'bianca', cheese: true, toppings: ['mushroom', 'thyme'] } },For anything else, art picks a drawing: knots, sweet-knots, bread, wings, burrata,
potatoes, salad, dip (with color and swirl), tiramisu, cookie, gelato, can or
bottle (with color and band). diet uses vegan, vegetarian, gf and spicy; for pizzas
gf means "can be made on the gluten-free base" and shows as GF base. allergens uses the keys
of ALLERGENS. Items with best: true appear in the home page bestsellers.
Prices are never stored in the cart, so changing a price updates every page, saved cart and total.
Addresses, phone numbers and the "Free delivery over £35" announcement bar are also written in the
patterns (header, footer, shops, shop-details): update them there when you change STORES or
FREE_DELIVERY_OVER.
Edit assets/js/main.js in a child theme (copy it there and load it instead), or your menu is
overwritten when you update the theme. Item names, descriptions and other data are not
translatable strings.
Images and artwork#
There are no photos. Pizzas, sides, dips, desserts and drinks are drawn as SVG by Art.pizza() and
Art.food() in assets/js/main.js, from each item's art settings, so new menu items get a
picture automatically. Any element with data-pizza="<menu id>" is filled with that item's
picture: add one in a Custom HTML block anywhere,
<div class="art size-40" data-pizza="hot-honey" aria-hidden="true"></div>The hero pizza is assets/img/hero-pizza.svg (square; replace it with any square image in the
home-hero Custom HTML block, keeping the alt text meaningful). The 404 page's pizza is the same
file in an Image block. The coloured drawings in the how-it-works steps, the dough steps, the
suppliers and the team portraits are inline SVG in Custom HTML blocks: replace one with an Image
block to use a photo (keep the circle by giving the image the same rounded classes).
The logo is assets/img/logo.svg (and logo-footer.svg on the dark footer) 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
Slice collection or WordPress's own. The social image is assets/img/og-image.png
(1200 × 630, source og-image.svg); the favicon assets/img/favicon.svg is used until you set a
Site Icon in Settings > General.
Navigation, header and footer#
The header and footer are template parts (parts/header.html, parts/footer.html) showing the
header and footer patterns. Edit them in Appearance > Editor > Patterns > Header / Footer.
The header pattern starts with the announcement bar.
The header has two Navigation blocks showing the same menu: the desktop one and the one in the
mobile menu. Edit the menu once in Appearance > Editor > Navigation (the setup creates "Main
menu"). 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 Order now button is a Button block below it.
The footer's Eat menu 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#
Delivery checker#
Any <div data-zone-checker></div> becomes a Delivery / Pickup switch with a postcode check
(delivery) or shop picker (pickup). Add data-variant="compact" for the small version used in the
cart drawer. It's on the home page, the locations page, the checkout and in the drawer, and every
copy stays in sync.
checkPostcode() reads the district from a UK postcode (M4 6AJ → M4, a bare M21 also works)
and finds the zone in ZONES that lists it. The result shows the shop, fee, minimum and delivery
time, and warns when that shop is closed. A valid postcode outside every zone gets a "we don't
deliver there yet" message. The choice is saved (see Saved data) and
shown in the header ("Delivery · M4 6AJ"). Press Enter in the postcode field to check; the result
is a live region.
Open now and opening hours#
storeStatus() compares the current time in TIMEZONE (not the visitor's clock) with
STORES[].hours. It fills any element with data-store-status="<store id>" ("Open until 22:00",
"Closing soon, last orders 22:00", "Opens today 16:00" or "Opens Tuesday 16:00"),
data-today-hours="<store id>" and <tbody data-hours="<store id>"> (a row per day, Monday
first, today highlighted). They refresh every minute.
Menu filters and section nav#
initMenu() renders the menu from SECTIONS and MENU into #menu-sections. The chips with
data-diet filter items that have all selected diets; empty sections and their nav links hide, the
count updates in a live region and "Clear" resets. The URL follows the filters, so links like
/menu/?diet=vegan or /menu/?diet=vegan,spicy work (gluten-free, veggie and hot are
accepted too, in any letter case; the footer's Vegan link uses it). /menu/?item=margherita opens
that item straight away. The section nav sticks under the header and highlights the section in
view, scrolling itself sideways on phones.
Item customiser#
openCustomiser() opens a <dialog> for a pizza: size (with the price of each size), crust, half
and half, up to six extra toppings and quantity. The picture, price, diet tags and allergens
update as you choose. Rules come from the data:
- Crusts with
sizes(gluten-free is Small only, stuffed crust is Regular and Large) switch the size for you, with a note explaining why; picking a size the crust doesn't come in switches back to classic. - Half and half is Regular and Large only and is charged at the dearer half (
pizzaUnit()). - Extras add
SIZES[].extraeach; meat, fish, dairy and honey extras remove the vegan / vegetarian tags (pizzaDiet()), and their allergens are added (pizzaAllergens()).
Sides, dips, desserts and drinks have no options, so their button adds one straight to the cart.
Any button with data-add="<menu id>" does the right thing. The dialog is the native <dialog>
element, so focus stays inside it, Esc closes it and focus returns to the button that opened it. On
phones it opens as a bottom sheet.
Cart drawer#
The cart opens from the header's order button, "Delivery or pickup?" in the header, the mobile
"View your order" bar and any [data-cart-open] element. It shows the delivery checker, each line
with picture, options, quantity stepper, Edit (pizzas and deals reopen their dialog) and
Remove, notes for the kitchen (200 characters), a promo code field and the totals. Above the
checkout button it shows a minimum-order warning for the zone, free-delivery progress, or a prompt
to add a postcode; the checkout button is disabled until the order can go through.
totals() works out everything: subtotal, promo discount, delivery fee (free over
FREE_DELIVERY_OVER or with a delivery code; pickup is always free), the zone minimum and the
total. The drawer traps focus, closes on Esc or the backdrop, and keeps focus on the same control
when lines change. Without JavaScript the order button goes to the checkout page.
Promo codes#
PROMOS holds the demo codes, applied in the drawer or at checkout:
| Code | Type | Effect |
|---|---|---|
SLICE10 |
percent |
10% off menu items (not deals or bundles) |
FREEDELIVERY |
delivery |
Free delivery |
FIRSTSLICE |
amount |
£5 off orders of £25 or more |
One code at a time; codes are read in any letter case. Unknown codes show an inline error. The
promo-codes pattern lists them with Copy buttons (data-copy="SLICE10"). Codes are checked in the
browser, so anyone can see them in the source: a real shop must validate codes on its server.
Deals and bundles#
DEALS defines set deals. A slot is either a choice
({ choose: 'pizzas', size: 'regular', label: 'Pizza one' } or any section id) or a fixed item
({ item: 'garlic-knots' }). art lists the pizzas drawn on the deal card. The deal dialog
(openDeal()) shows a select per choice and how much the deal saves against the same items bought
separately (dealWorth()). Deal pizzas are classic crust with no extras.
The bundle builder lets you pick Regular pizzas, sides, desserts and drinks from
BUNDLE_SECTIONS, up to BUNDLE_MAX items. The best tier in BUNDLE_TIERS you qualify for applies
to the whole bundle (two pizzas and a side for 15% off, three pizzas and two sides for 20%), with a
meter and a hint for the next tier. Add bundle is enabled once a tier is unlocked. Bundles
appear in the cart as one line. The tiers' descriptions in the bundle Custom HTML are text: keep
them in step with BUNDLE_TIERS.
Checkout and time slots#
The checkout page shows the empty state (a block section you can edit), the form, or the confirmation of the latest order. The form has four steps:
- Delivery or pickup: the delivery checker.
- When: "As soon as possible" is offered only while the shop is open and can still make the
order today; otherwise "Schedule for later" is chosen.
timeSlots()builds slots everySLOT_STEPminutes for today and the next days (SCHEDULE_DAYS): the first slot is opening time plus the lead time (pickupMinsordeliveryMins), or now plus the lead time for today, and the last is closing time. - Your details: name, mobile, email, and for delivery a street address and rider instructions.
- Payment (demo): card (number in groups of four, expiry
MM / YY, CVC) or pay on delivery / at the counter.
On submit the fields are validated with inline errors and the order is checked against the
delivery minimum and postcode. A valid order is saved as the latest order, the cart is cleared and
the confirmation is shown. No payment is processed. The submit handler in initCheckout()
builds a placed object with the lines, totals, mode and time and marks the spot for your ordering
system or payment provider (for example a Stripe Checkout session created by your server): only
show the confirmation when it succeeds, recalculate prices on the server, and don't collect card
numbers in your own form.
Order tracker#
The confirmation shows the order number, a summary and a tracker with four stages (received, in
the oven, out for delivery, delivered; for pickup the last two are "Boxed & ready" and
"Collected"). In the demo each stage lasts TRACKER_STEP_SECONDS (8 seconds) from the time the
order was placed, so it keeps its place if you reload. A rider (or bag) icon moves along the bar,
the current stage pulses and the status line is a live region. Stages are in STAGES; to show
real progress, replace the timer in renderTracker() with updates from your kitchen or delivery
system. Start a new order clears it.
Locations map#
The map in the delivery-zones pattern is inline SVG in a Custom HTML block. Each zone is a
<path data-map-zone="<zone id>">; its fill is also used for the swatch in the zones table,
which is built from ZONES. Hovering a row or zone highlights it, and a checked postcode
highlights its zone. The map has a text description; the table carries the same information. To
draw your own area, replace the paths and pins and keep the data-map-zone ids in step with
ZONES.
Catering calculator#
On the catering page, guests (10–300) and appetite (APPETITE: slices per person) give the
number of large pizzas (8 slices each), a suggested veggie / vegan / meaty mix, sides and an
estimate using the average large pizza price and CATERING_DISCOUNT (10% off 10+ pizzas, 15% off
20+). Use these numbers copies the guest count and a summary into the enquiry form.
Scroll reveal, motion and back to top#
Blocks with reveal fade in as they scroll into view. The hero ingredients float (bob), the
round badges spin and menu-card pizzas turn on hover. All motion is off for visitors who prefer
reduced motion. A back-to-top button appears after one screen; it lifts above toasts and the
mobile order bar. 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 tomato button), Cheese, Cream, Ghost, Ink |
The classes is-px-8, 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 and call-to-action buttons use them).
FAQ answers on the locations and catering 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)
and show a success state. 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", type="number" with min and max, type="date"
with data-min-days, type="time" with min and max, data-label (the name used in the
message), data-validate (postcode, card, expiry, cvc) and, for a group of radio buttons,
data-group with data-msg.
| Form | Where | Validates |
|---|---|---|
Newsletter (data-form="newsletter") |
Footer of every page | |
Catering enquiry (data-form="catering") |
Catering | Name, email, phone, guests 10–300, date at least two days ahead (in the shop's time zone), time 11:00–22:00, delivery or pickup, delivery postcode (UK format, only when delivering), consent |
Checkout (#checkout-form) |
Checkout | Delivery postcode in a zone, minimum order, a time slot, name, phone, email, address (delivery), card fields (card payment) |
None of them send anything 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
assets/js/main.js (in a child theme), for example to Formspree:
$$('[data-form="catering"]').forEach((form) => {
setupValidation(form, () => {
fetch('https://formspree.io/f/your-id', {
method: 'POST',
body: new FormData(form),
headers: { Accept: 'application/json' }
}).then((r) => {
if (r.ok) successSwap(form, '<p class="h-display text-3xl">Thanks!</p><p class="mt-3">We’ll reply within one working day.</p>');
else $('[data-form-status]', form).textContent = 'Sorry, that didn’t send. Please try again or call us.';
});
});
});Every field has a name, so the request carries all the values; keep a name on any field you add.
Saved data (localStorage)#
| Key | Shape | Notes |
|---|---|---|
crustclub-order |
{ mode: 'delivery' | 'pickup', postcode: 'M4 6AJ', store: 'ancoats' } |
Delivery or pickup choice. The zone is worked out again from the postcode on every read. |
crustclub-cart |
{ lines: [...], promo: 'SLICE10', notes: '...' } |
Lines are { type: 'pizza', id, size, crust, half, extras, qty }, { type: 'item', id, qty }, { type: 'deal', id, picks, qty } or { type: 'bundle', items: [{ id, qty }], qty }. |
crustclub-last-order |
The confirmation: number, time placed, mode, summary, lines and totals | Shown on the checkout page for six hours while the cart is empty. |
These are the HTML version's keys. Every read is validated by normalizeLine() and Cart.get():
unknown items, sizes, crusts, toppings and codes are dropped, crusts and half-and-half are
corrected for the size, quantities are clamped to 1–MAX_QTY and duplicate lines are merged.
Prices are always recalculated from MENU. Other open tabs update through the storage event. To
reset, clear the site's storage in the browser's developer tools, or run
localStorage.removeItem('crustclub-cart') in the console. 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 "Menu —
Crust Club"); 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. For local search,
consider adding Restaurant structured data (JSON-LD) with each shop's address, phone,
openingHours and hasMenu.
The menu is rendered by JavaScript from MENU. Search engines that run JavaScript (such as Google)
see it; if you need the menu in the page's HTML, add it as blocks to the menu-list section.
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 everywhere
(yellow on red and dark bands), the menu button with aria-expanded and a label that changes with
its state, native <dialog> customiser and deal pickers, a cart drawer (role="dialog",
aria-modal) that traps focus and returns it, diet filter chips as toggle buttons
(aria-pressed), live regions for the menu count, postcode results, bundle hint, toasts and
tracker status, labelled quantity steppers ("One more Margherita"), diet tags with text, written
allergens, 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, keep text on Tomato and Basil at full opacity, 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-slice, 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-slice. All text in patterns,
templates and PHP goes through WordPress's translation functions; the script's strings (delivery
checker, opening times, customiser, cart, checkout, tracker and form messages) are translated in
PHP (freeframe_slice_script_strings() in inc/setup.php). Menu items, deals, shops, zones and
promo labels 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-slice wp-content/themes/freeframe-slice/languages/freeframe-slice.potSave freeframe-slice-fr_FR.mo (for example) in wp-content/languages/themes/ or in the theme's
languages/ folder. Page content created by Slice 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 Slice is updated (your menu in particular).
Create wp-content/themes/slice-child/style.css:
/*
Theme Name: Slice Child
Template: freeframe-slice
*/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-slice) 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 > Slice.
- The menu is empty or nothing happens when I click "Add". JavaScript isn't running: check the
browser console for errors after editing
main.js(a missing comma inMENUis the usual culprit), and make sure themenu-listsection's#menu-sectionsblock is still there. - "Open now" is wrong. Hours are in
TIMEZONE, not the server's or visitor's time zone. CheckSTORES[].hours(Sunday first) and setTIMEZONEto your shop's zone. - My postcode isn't recognised. Zones match UK postcode districts. For other countries, change
postcodeDistrict()andcheckPostcode()to read your format and list those codes inZONES[].districts. - Links in the cart go to a page that doesn't exist. The script links to
/menu/,/deals/and/checkout/. If you renamed those pages, updatefreeframe_slice_page_urls()ininc/setup.php. - Slice 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 > Slice pages.
- 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, allergen policy), 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. All artwork (pizzas, food, map, logo and
illustrations) is included under the same license. Rubik and Shrikhand are licensed under the SIL
Open Font License 1.1 (OFL.txt in each folder of assets/fonts/).