Freeframe

Documentation

Slice

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

Overview#

Slice is a website for a pizzeria or takeaway with online ordering, built for Crust Club, a fictional sourdough pizzeria with two shops in Manchester (Ancoats and Chorlton). It has a filterable menu with a pizza customiser, a cart drawer on every page, a postcode delivery checker, set deals and a bundle builder, a checkout with time slots worked out from the opening hours, an animated order tracker, a locations page with a delivery-zone map, an about page and a catering page with a pizza calculator and enquiry form. This is the Next.js version: Next.js 16 (App Router), React 19, TypeScript and Tailwind CSS v4, exported as a fully static site. The menu, bestsellers and deals are rendered at build time from typed data, so they are in the HTML. This guide covers version 1.0.0. See it live at freeframe.dev/templates/slice.

Quick start#

Requirements: Node.js 24 or newer and pnpm (npm works too).

sh
pnpm install
pnpm dev        # http://localhost:3000

Try an order: check the postcode M4 6AJ on the home page, add a pizza from the menu, apply the code SLICE10 in the cart and place the demo order. Nothing is sent or charged.

pnpm build writes the static site to out/; pnpm start serves it.

Pages#

Route File Purpose Notable sections and features
/ src/app/page.tsx Home Hero with delivery / pickup switch and postcode checker, bestsellers (from the menu data), deals, how ordering works, dough story, reviews, both shops with open-now badges
/menu src/app/menu/page.tsx Menu Sticky section nav with scrollspy, vegan / vegetarian / gluten-free / spicy filters, live count, item customiser, allergen guide (/menu#allergens)
/deals src/app/deals/page.tsx Deals & bundles Three set deals with a picker dialog, build-your-bundle with discount tiers (/deals#bundle), demo promo codes with copy buttons
/checkout src/app/checkout/page.tsx Checkout (demo) Delivery or pickup, ASAP or scheduled slot, contact and address, demo payment, order summary with promo code, confirmation with order tracker, empty state
/locations src/app/locations/page.tsx Locations & delivery SVG delivery-zone map, postcode checker, zones table, both shops with full hours (today highlighted) and open-now, delivery FAQ
/about src/app/about/page.tsx Our story Timeline, the four dough steps, suppliers, team, values
/catering src/app/catering/page.tsx Catering & party orders Packages, pizza calculator, party-order enquiry form, FAQ
any other src/app/not-found.tsx Not found (404) Links to the menu, home and locations

Project structure#

slice/
  src/
    app/
      layout.tsx            html shell: fonts, announcement bar, header, footer, cart drawer, dialog, toast
      fonts.ts              Rubik and Shrikhand via next/font/local
      globals.css           Tailwind v4 @theme tokens and component classes
      icon.svg              favicon
      page.tsx              home page
      menu/ deals/ checkout/ locations/ about/ catering/   one folder per page (page.tsx)
      not-found.tsx         404 page
    components/
      SiteHeader.tsx        header, order label and button, mobile menu (client)
      SiteFooter.tsx        footer with shop details and newsletter
      MenuCard.tsx          a menu item card (server)
      DealCard.tsx          a set-deal card (server)
      Art.tsx               an item's SVG picture
      DietTags.tsx          vegan / vegetarian / GF / spicy badges
      StoreStatus.tsx       open-now badge, today's hours and the hours table (client)
      icons.tsx             shared inline SVG icons
      RevealObserver.tsx    scroll reveals
      BackToTop.tsx         back-to-top button
      order/                OrderProvider (drawer, dialog, toast, promo codes), CartDrawer, ItemSheet
                            (customiser and deal picker), ZoneChecker, CartParts, OrderBar, Toast, buttons
      menu/MenuFilters.tsx  diet chips, count, scrollspy nav and filterable sections and cards
      deals/BundleBuilder.tsx
      checkout/             CheckoutView (form and states) and OrderTracker (confirmation)
      locations/ZoneHighlight.tsx   map and zone table highlighting
      catering/Catering.tsx calculator and enquiry form
      forms/                useValidation hook, NewsletterForm, SuccessBox
    data/
      shop.ts               shops, hours, delivery zones, promo codes and order settings
      menu.ts               sizes, crusts, toppings, menu items, deals and bundle tiers
      catering.ts           calculator settings
      site.ts               header, mobile and footer links
    lib/
      art.ts                the SVG drawing code (pizzas and food)
      order.ts              postcodes, the saved order and cart (localStorage), line info and totals
      pricing.ts            pizza, deal and bundle prices, diet tags and allergens
      hours.ts              shop clock, open-now status and time slots
      validation.ts         form field rules
      format.ts             money and number helpers
      metadata.ts           per-page title, description and Open Graph tags
      asset.ts              base-path helper for files in public/
      use-shop-now.ts, use-hydrated.ts, storage.ts
    fonts/                  Rubik and Shrikhand woff2 files and their licenses
  public/img/               hero-pizza.svg, og-image.png and its source og-image.svg
  next.config.ts            static export and optional base path
  template.json             gallery metadata (not part of your site)

Customising#

Brand and colours#

All colours and fonts are tokens in the @theme block at the top of src/app/globals.css. Each token becomes Tailwind classes (--color-tomato gives bg-tomato, text-tomato, ring-tomato...).

Token Default Used for
--color-mozz #fff4e2 Page background
--color-paper #fffaf2 Cards and fields
--color-flour #f9e6c8 Soft panels, steppers
--color-tomato #cc2a16 Brand red: hero, primary buttons, menu header
--color-tomato-deep #94190b Button lip, red text on cream
--color-cheese #ffc23a Yellow: deals band, stickers, secondary buttons
--color-basil #1d6b3c Green: vegan tags, success, dough band
--color-ink #2a1209 Text, footer, dark buttons
--color-cocoa #6b4a3a Secondary text

Each strong colour has a -soft tint (tomato-soft, cheese-soft, basil-soft) for tags and card backgrounds. To re-brand, change the values:

css
@theme {
  --color-tomato: #1f4fd1; /* a blue pizzeria */
  --color-tomato-deep: #12307f;
}

Keep text on tomato and basil at 4.5:1 contrast or better. A few colours are written directly in SVG artwork (pizza toppings in src/lib/art.ts, the map zones in ZONES[].color, og-image.svg) and in themeColor in src/app/layout.tsx.

Component classes live in @layer components in globals.css: btn-primary, btn-cheese, btn-basil, btn-ink, btn-ghost, btn-cream, card, tag with tag-vegan / tag-vegetarian / tag-gf / tag-spicy, field, option (radio cards), chip (filter and checkbox pills), segmented (the Delivery / Pickup switch), stepper, checker (the tablecloth band) and scallop-b (the scalloped bottom edge of hero bands).

Typography#

Rubik (text, variable weights 400–900) and Shrikhand (display) are self-hosted with next/font/local in src/app/fonts.ts, from the same woff2 files Google Fonts serves (latin and latin-ext subsets, in src/fonts/), so nothing loads from Google. The files register the family names Rubik and Shrikhand, which the tokens use:

css
--font-sans: "Rubik", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--font-display: "Shrikhand", "Rubik", ui-sans-serif, system-ui, sans-serif;

font-display (and the h-display class) is for big headings and the logo; everything else uses Rubik. To swap a font, put its woff2 files in src/fonts/, change the src and font-family declarations in fonts.ts and the token in globals.css. Shrikhand has a single weight, so keep it for large text. You can also use next/font/google instead, but it may serve different files from the ones the HTML version uses.

Content#

Page text lives in the page files. Everything you can order lives in typed data in src/data/:

File Export What it holds
shop.ts STORES Shops: id, name, address, phone, hours (Sunday first, ["12:00", "22:00"] or null), pickupMins, deliveryMins
shop.ts ZONES Delivery zones: postcode districts, the store that serves them, fee, min order, eta range in minutes and map color
shop.ts FREE_DELIVERY_OVER Subtotal (after promo) that makes delivery free (35)
shop.ts PROMOS Demo promo codes (see Promo codes)
menu.ts SECTIONS Menu sections in order, with the blurb under each heading
menu.ts SIZES Pizza sizes: delta from the Regular price and extra (price of each extra topping)
menu.ts CRUSTS Crusts: price, optional sizes they're limited to, allergens, notVegan, glutenFree
menu.ts TOPPINGS Extra toppings: kind (veg, dairy, honey, meat, fish) decides vegan / vegetarian tags
menu.ts MENU Every item: section, name, price, desc, diet, allergens, art, optional best
menu.ts DEALS Set deals with their slots
menu.ts BUNDLE_TIERS Bundle discount tiers
catering.ts APPETITE, CATERING_DISCOUNT Calculator settings

Other settings in shop.ts: TIMEZONE ("Europe/London"), LOCALE and CURRENCY (prices are formatted with Intl.NumberFormat in src/lib/format.ts), 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:

ts
{ 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. TypeScript checks the shapes, so a typo in a diet or allergen fails pnpm typecheck.

Prices are never stored in the cart, so changing a price updates every page, saved cart and total.

Images and artwork#

There are no photos. Pizzas, sides, dips, desserts and drinks are drawn as SVG by src/lib/art.ts (pizza() and food(), the same code as the HTML version), from each item's art settings, so new menu items get a picture automatically. Topping drawings are in the PIECES object; add a key there to use a new topping in art. Positions are seeded from the item id, so the same pizza always looks the same, and the pictures drawn at build time match the ones drawn in the browser (the customiser preview and the cart).

<Art item="margherita" className="size-40" /> (src/components/Art.tsx) shows an item's picture anywhere, for example the decorative pizzas in page headers. public/img/hero-pizza.svg is a static file drawn with the same code; replace it with any square image. If you prefer photos, change itemArt() in art.ts (or Art) to return an <img>: menu cards show art in a square, the cart in a 64–80 px square.

The social image is public/img/og-image.png (1200×630). Edit og-image.svg, then export it as a PNG at the same size, because most social sites don't display SVG. The favicon is src/app/icon.svg.

The header (SiteHeader.tsx), footer (SiteFooter.tsx), cart drawer, dialog and toast are in the layout, so they appear on every page. Menu links come from src/data/site.ts: NAV (desktop), MOBILE_NAV (adds Home) and FOOTER_NAV. To add a page, create src/app/<route>/page.tsx (export metadata with pageMetadata()) and add it to those lists; the current page is marked with aria-current="page" automatically. Use next/link for internal links.

Shop names, addresses and phone numbers in the footer and zone checker come from STORES; the shop cards on the home and locations pages are written in the page files, so change them there too.

Features#

Delivery checker#

<ZoneChecker /> (src/components/order/ZoneChecker.tsx) is a Delivery / Pickup switch with a postcode check (delivery) or a shop picker (pickup). <ZoneChecker compact /> is the small version in the cart drawer. It's on the home page, the locations page, the checkout and in the drawer, and every copy shows the same saved order.

checkPostcode() in src/lib/order.ts 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. Press Enter in the field to check. The choice is saved (see Saved data) and shown in the header. The radios and the postcode field have labels; the result is a polite live region.

Open now and opening hours#

storeStatus() in src/lib/hours.ts compares the current time in TIMEZONE (not the visitor's clock) with STORES[].hours. <StoreStatus store="ancoats" /> shows "Open until 22:00", "Closing soon, last orders 22:00", "Opens today 16:00" or "Opens Tuesday 16:00" with a coloured dot; <TodayHours /> and <HoursRows /> show today's hours and a table, Monday first, with today highlighted. They read the clock with useShopNow(), which updates every minute. The prerendered HTML can't know the time, so these are empty until the page loads (no hydration mismatch).

The menu page renders every section and card on the server from SECTIONS and MENU. The client components in src/components/menu/MenuFilters.tsx add the filters: the chips (aria-pressed toggle buttons) show 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). /menu?item=margherita opens that item straight away.

The section nav sticks under the header and highlights the section in view (an IntersectionObserver scrollspy).

Item customiser#

openCustomiser(id) from useOrderUI() (OrderProvider.tsx) opens the dialog in src/components/order/ItemSheet.tsx 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() in src/lib/pricing.ts).
  • Extras add SIZES[].extra each; meat, fish, dairy and honey extras remove the vegan / vegetarian tags (pizzaDiet()), and their allergens are added (pizzaAllergens()).

For example a Large, half Margherita / half Green Machine with extra nduja costs £18.50 and is tagged Spicy only. Sides, dips, desserts and drinks have no options, so <AddButton id="cola"> adds one straight to the cart with a toast; for pizzas it opens the customiser.

The dialog is the native <dialog> element, so focus stays inside it, Esc or a click on the backdrop closes it and focus returns to the button that opened it. On phones it opens as a bottom sheet.

Cart drawer#

CartDrawer.tsx opens from the header button, the mobile "View your order" bar (OrderBar.tsx) and any <CartOpenButton>. It shows the delivery checker, each line with picture, options, quantity stepper, Edit (pizzas and deals reopen their dialog with "Update item"; the drawer comes back afterwards) and Remove, notes for the kitchen (200 characters, saved as you type), 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 (and focuses the postcode) until the order can go through.

totals() in src/lib/order.ts 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, returns focus to the button that opened it, and keeps focus on the same control when lines change.

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. Unknown codes show an inline error linked to the field. 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 (DealCard.tsx, rendered on the server). The deal picker (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.

BundleBuilder.tsx 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.

Checkout and time slots#

CheckoutView.tsx shows the empty state, 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. Nothing is sent: see Forms for where to connect a real order system.

Order tracker#

OrderTracker.tsx shows the order number (CC-1234), 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 OrderTracker with updates from your kitchen or delivery system. Start a new order clears it.

Locations map#

The map on the locations page is inline SVG. Each zone is a <MapZone id d> path filled with ZONES[].color, which is also used for the swatch in the zones table built from ZONES. Hovering a row or zone highlights it in both (ZoneHighlight.tsx), and a checked postcode highlights its zone. The map is decorative (it has a text description); the table carries the same information. To draw your own area, replace the paths and pins in src/app/locations/page.tsx and keep the 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 and moves focus to it.

Scroll reveal, motion and back to top#

Elements with .reveal fade in as they scroll into view (RevealObserver.tsx); anything above them is revealed too, so jumping down the page never leaves gaps. The hero ingredients float (.bob), the round badge spins and menu-card pizzas turn on hover. All motion is off for visitors who prefer reduced motion. The back-to-top button (BackToTop.tsx) appears after one screen; it lifts above the toast and the mobile order bar and hides while it would cover the bundle summary. Remove it from layout.tsx and the .back-to-top styles to drop it.

Forms#

All forms validate in the browser with inline messages (rules in src/lib/validation.ts, wiring in src/components/forms/useValidation.tsx) and show a success state. None of them send anything until you connect them.

Form Where Validates
Newsletter (data-form="newsletter") src/components/forms/NewsletterForm.tsx, footer of every page Email
Catering enquiry (data-form="catering") EnquiryForm in src/components/catering/Catering.tsx 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) CheckoutForm in src/components/checkout/CheckoutView.tsx Delivery postcode in a zone, minimum order, a time slot, name, phone, email, address (delivery), card fields (card payment)

Rules come from attributes on each field: required and data-label ("Email is required."), the input type, data-validate (postcode, card, expiry, cvc), min / max and data-min-msg. Errors appear in <p id="<field id>-error"> right after the field, linked with aria-describedby and aria-invalid, and the first problem is focused.

To connect the enquiry form to a form service such as Formspree, post the data in its onSubmit before showing the thank-you message:

tsx
onSubmit={async (e) => {
  e.preventDefault();
  const form = e.currentTarget;
  if (!v.validate(form)) return setStatus("Please fix the highlighted fields.");
  const res = await fetch("https://formspree.io/f/your-id", {
    method: "POST",
    body: new FormData(form),
    headers: { Accept: "application/json" },
  });
  if (!res.ok) return setStatus("Sorry, that didn’t send. Please try again or call us.");
  setSent({ /* ... as before ... */ });
}}

The newsletter works the same way in NewsletterForm.tsx. If you remove the static export (see Building and deploying), you can post to your own Route Handler (for example src/app/api/enquiries/route.ts exporting POST) or call a Server Action instead.

For real online ordering, submit() in CheckoutForm builds a placed object with the lines, totals, mode and time; send that to your ordering system or payment provider (for example a Stripe Checkout session created by your server) where the comment says so, and only show the confirmation when it succeeds. Never trust prices from the browser: recalculate them on the server.

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 /checkout for six hours while the cart is empty.

Every read is validated by readOrder(), normalizeLine() and readCart() in src/lib/order.ts: 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. Components read the order with useOrderState() (useSyncExternalStore), which returns an empty cart while the page is prerendered and hydrated, and 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#

  • Site-wide defaults are in metadata in src/app/layout.tsx (they are also the home page's title and description).
  • Every other page exports metadata = pageMetadata(title, description) from src/lib/metadata.ts, which sets the title, description, Open Graph tags (with the image asset("/img/og-image.png")) and the Twitter card.
  • metadataBase comes from NEXT_PUBLIC_SITE_URL (falling back to https://example.com). Set it when you build so social images get absolute URLs:
sh
NEXT_PUBLIC_SITE_URL=https://www.your-pizzeria.co.uk pnpm build

The menu, bestsellers and deals are in the exported HTML, so every search engine sees them. Consider adding src/app/sitemap.ts and src/app/robots.ts (both work with static export) and Restaurant structured data as JSON-LD with each shop's address, phone, openingHours and hasMenu.

Accessibility#

  • Skip link, landmarks (header, nav, main, footer), one h1 per page and a logical heading order.
  • Visible focus outlines everywhere, including radio cards, chips and the Delivery / Pickup switch.
  • The mobile menu button has aria-expanded and closes on Esc.
  • The customiser and deal pickers use the native <dialog>; the cart drawer is role="dialog" with aria-modal, a focus trap and Esc to close.
  • Filter chips are toggle buttons (aria-pressed); the result count, postcode result, bundle hint, toasts and tracker status are live regions.
  • Quantity steppers have labelled buttons ("One more Margherita"), and remove / edit buttons include the item name for screen readers.
  • Diet tags have text, not just colour; allergens are written out.
  • Form errors are linked with aria-describedby, fields get aria-invalid and the first error is focused.
  • Reduced motion turns off reveals, floating, spinning, card turns and tracker animations.
  • Colours meet WCAG AA contrast. Keep text on tomato and basil at full opacity when you edit.

When editing, keep exactly one h1 per page, give every new input a <label>, and keep alt text on meaningful images.

Building and deploying#

pnpm build writes a fully static site to out/ (output: "export" in next.config.ts). Deploy it anywhere:

  • Vercel: import the repository; no settings needed.
  • Netlify / Cloudflare Pages: build command pnpm build, publish directory out.
  • Any static host: upload the contents of out/.

To serve the site from a sub-folder, build with a base path: DEMO_BASE_PATH=/slice pnpm build. next.config.ts passes it to basePath and to NEXT_PUBLIC_BASE_PATH, which asset() uses. pnpm build:demo does this for the freeframe.dev demo and writes demo/, which you can delete.

Need server features (a Route Handler for orders, Server Actions, middleware)? Remove output: "export" and images: { unoptimized: true } from next.config.ts and deploy to a Node-capable host such as Vercel. Everything else keeps working.

pnpm lint runs ESLint 10 and pnpm typecheck runs TypeScript 7 (@typescript/native); the typescript package is an alias of @typescript/typescript6, because typescript-eslint still needs the TypeScript 6 API.

Troubleshooting#

  • "Open now" is wrong. Hours are in TIMEZONE (src/data/shop.ts). Check STORES[].hours (Sunday first) and set TIMEZONE to your shop's zone, for example "America/New_York".
  • My postcode isn't recognised. Zones match UK postcode districts. For other countries, change postcodeDistrict() and checkPostcode() in src/lib/order.ts to read your format (for example a 5-digit ZIP) and list those codes in ZONES[].districts.
  • The cart shows old items or totals after I changed the menu. Lines whose item, size or topping no longer exists are removed on the next read. Clear crustclub-cart to start fresh.
  • There are no time slots. The shop is closed for the next SCHEDULE_DAYS days, or the lead time runs past closing. Check the hours, pickupMins and deliveryMins.
  • A new class has no effect. Tailwind only finds full class names in the source; write bg-basil, not "bg-" + colour.
  • Images or links break under a sub-folder. Build with DEMO_BASE_PATH and reference public files with asset() (links made with next/link get the base path automatically).
  • Social previews point to example.com. Set NEXT_PUBLIC_SITE_URL when building.
  • Forms "send" but nothing arrives. Connect them as described in Forms.

License#

MIT: the full text is in LICENSE.txt, included in the download. You can use, change and sell sites built with this template, including all artwork (pizzas, food, map, logo and illustrations). Rubik and Shrikhand are licensed under the SIL Open Font License (src/fonts/OFL-rubik.txt, src/fonts/OFL-shrikhand.txt). Crust Club, its team and its customers are fictional.

Search documentation

Search the guides and every template's documentation