Freeframe

Documentation

Slice

v1.0.2Updated Oct 16, 2026A fun, punchy pizzeria website with online ordering, delivery zones and a live order tracker.

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.

  1. Download slice.zip from freeframe.dev/templates/slice (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 Slice.
  3. 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.
  4. Visit your site. Edit any page in Pages, and the header, footer and templates in Appearance > Editor.

You can also try the theme without installing anything: the Try it in WordPress button on freeframe.dev opens it in WordPress Playground (WordPress running in your browser) with the demo pages already set up.

To work on the theme's files locally you need Node.js 24 and pnpm; WordPress itself runs in WordPress Playground, so there is no PHP, web server or database to install:

sh
pnpm install
pnpm dev     # builds the CSS, then starts WordPress with the theme at http://127.0.0.1:9400

pnpm dev signs you in as admin (password password) and creates the demo pages. The site is rebuilt from scratch every time you start it; your edits to theme files are live, edits made in the WordPress admin are not kept.

Pages#

Each page is a page pattern (in Patterns > 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.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
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-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.
  • 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 draws the template's rounded card; eyebrow the small red heading above titles; h-display the Shrikhand heading style; on-red and on-dark switch focus rings to yellow.
  • scallop-b cuts the scalloped bottom edge of the page headers; checker draws the checkered band above the footer; ff-checker-bands draws 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:

js
{ 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,

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

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.

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[].extra each; 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:

  1. Delivery or pickup: the delivery checker.
  2. 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 every SLOT_STEP minutes for today and the next days (SCHEDULE_DAYS): the first slot is opening time plus the lead time (pickupMins or deliveryMins), or now plus the lead time for today, and the last is closing time.
  3. Your details: name, mobile, email, and for delivery a street address and rider instructions.
  4. 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 Email
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:

js
$$('[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/:

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-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:

sh
wp i18n make-pot wp-content/themes/freeframe-slice wp-content/themes/freeframe-slice/languages/freeframe-slice.pot

Save 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:

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 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 > 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 in MENU is the usual culprit), and make sure the menu-list section's #menu-sections block is still there.
  • "Open now" is wrong. Hours are in TIMEZONE, not the server's or visitor's time zone. Check STORES[].hours (Sunday first) and set TIMEZONE to your shop's zone.
  • My postcode isn't recognised. Zones match UK postcode districts. For other countries, change postcodeDistrict() and checkPostcode() to read your format and list those codes in ZONES[].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, update freeframe_slice_page_urls() in inc/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/).

Search documentation

Search the guides and every template's documentation