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 Astro version: Astro 7, TypeScript and Tailwind CSS v4, built to a static site with no UI framework. The menu, deal cards, bundle list and zone table are rendered from typed data at build time, 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).
pnpm install
pnpm dev # http://localhost:4321With npm: npm install, then npm run dev.
| Command | What it does |
|---|---|
pnpm dev |
Development server at http://localhost:4321 with hot reload |
pnpm build |
Static site in dist/ |
pnpm preview |
Serve dist/ locally after a build |
pnpm check |
astro check: type-checks .astro files and TypeScript |
pnpm typecheck |
Generate Astro's types, then type-check with tsc (TypeScript 7) |
pnpm lint |
ESLint (TypeScript and Astro rules) |
pnpm build:demo |
Build for /demos/astro/slice into demo/ (only used for the demo on freeframe.dev) |
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.
Pages#
| File | URL | Purpose and notable features |
|---|---|---|
src/pages/index.astro |
/ |
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 |
src/pages/menu.astro |
/menu |
Every section and card from MENU, sticky section nav with scrollspy, vegan / vegetarian / gluten-free / spicy filters, live count, empty state, item customiser, allergen guide (/menu#allergens) |
src/pages/deals.astro |
/deals |
Three set deals with a picker dialog, build-your-bundle with discount tiers (/deals#bundle), demo promo codes with copy buttons |
src/pages/checkout.astro |
/checkout |
Delivery or pickup, ASAP or scheduled slot, contact and address, demo payment, order summary with promo code, confirmation with order tracker, empty state |
src/pages/locations.astro |
/locations |
SVG delivery-zone map, postcode checker, zones table (from ZONES), both shops with full hours (today highlighted) and open-now, delivery FAQ |
src/pages/about.astro |
/about |
Timeline, the four dough steps, suppliers, team, values |
src/pages/catering.astro |
/catering |
Packages, pizza calculator, party-order enquiry form, FAQ |
src/pages/404.astro |
404.html |
Not found, with links to the menu, home and locations |
Project structure#
slice/
├─ src/
│ ├─ pages/ one .astro file per page (see above)
│ ├─ layouts/
│ │ └─ Layout.astro <head> (title, meta, Open Graph, fonts), <body data-page>, header, footer, cart drawer, scripts
│ ├─ components/
│ │ ├─ Logo.astro the logo link (pizza-slice badge + "Crust Club"), light or dark
│ │ ├─ SiteHeader.astro announcement bar, header, order button, mobile menu
│ │ ├─ SiteFooter.astro shops, footer links, newsletter form, copyright
│ │ ├─ CartDrawer.astro cart drawer, overlay and toast markup (filled by the script)
│ │ ├─ MenuCard.astro one menu item card (menu page and home bestsellers)
│ │ ├─ DealCard.astro one set-deal card (home and deals pages)
│ │ └─ PizzaArt.astro a menu item's SVG picture, drawn at build time
│ ├─ data/
│ │ ├─ shop.ts shops, opening hours, delivery zones, promo codes and other settings
│ │ ├─ menu.ts sections, sizes, crusts, toppings, menu items, deals, bundle tiers
│ │ └─ nav.ts header, mobile menu and footer links
│ ├─ lib/
│ │ ├─ site.ts site name, theme colour, language and the url() helper
│ │ ├─ art.ts the pizza and food artwork (SVG drawn from the menu data)
│ │ └─ menu.ts shared helpers: money format, lookups, diet badges, prices, itemArt()
│ ├─ scripts/
│ │ ├─ main.ts all interactivity: cart, delivery checker, customiser, menu, deals, checkout, tracker, forms
│ │ └─ back-to-top.ts the back-to-top button
│ └─ styles/
│ └─ global.css Tailwind import, brand tokens (@theme), component classes
├─ public/img/ favicon.svg, hero-pizza.svg, og-image.svg (source), og-image.png (social image)
├─ astro.config.mjs site URL, base path, file-style output
├─ eslint.config.mjs, tsconfig.json, package.json
└─ README.md, DOCS.md, CHANGELOG.mdCustomising#
Brand and colours#
All colours and fonts are tokens in the @theme block at the top of src/styles/global.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; the dev server picks them up straight away:
@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, og-image.svg), in the logo (Logo.astro) and in
themeColor in src/lib/site.ts (<meta name="theme-color">).
Component classes live in @layer components in global.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#
Two Google Fonts are loaded in src/layouts/Layout.astro:
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,400..900;1,400..700&family=Shrikhand&display=swap"
/>--font-display (Shrikhand, used through the h-display class and font-display) is for big headings and
the logo; --font-sans (Rubik) is for everything else. To swap them, change the link and the two tokens in
global.css. Shrikhand only has one weight, so keep it for large text.
Content#
Page text lives in the .astro files in src/pages/. Everything you can order lives in typed data:
| File | Export | What it holds |
|---|---|---|
src/data/shop.ts |
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) | |
src/data/menu.ts |
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 in shop.ts: 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. The interfaces at the top of each file (Store, Zone, MenuItem, Deal...)
describe every field, so your editor and pnpm check catch typos.
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 and get a Bestseller sticker on the menu.
The menu page, home bestsellers, deal cards, bundle list and zone table are built from this data when the site is built; the cart, customiser and checkout read the same files in the browser. Prices are never stored in the cart, so changing a price updates every page, saved cart and total.
The shops' addresses and phone numbers in the footer come from STORES; the shop cards on the home and
locations pages and the copy on the catering page are written in the pages, so change those too.
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()), 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. Pizza positions
are seeded from the item id, so the same pizza always looks the same, at build time and in the browser.
itemArt() in src/lib/menu.ts picks the picture for a menu item or a configured pizza. PizzaArt.astro
draws one into the page (used for the decorative pizzas in page headers):
<PizzaArt class="art absolute left-0 top-4 size-56 -rotate-12" item="pepperoni" />public/img/hero-pizza.svg is a static file drawn with the same code; replace it with any square image. If
you prefer photos, return an <img> from itemArt(): 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 public/img/favicon.svg.
Navigation, header and footer#
The header, mobile menu, footer, cart drawer and toast are components used by Layout.astro, so they're
written once. Links come from src/data/nav.ts: mainNav (header and mobile menu), homeLink (first in
the mobile menu) and footerNav (the footer's "Eat" list).
To add a page, create src/pages/<name>.astro with the layout, add it to mainNav with a key, and pass
that key as current so its links get aria-current="page":
<Layout title="Gift cards — Crust Club" description="…" current="gifts" page="gifts">
<main id="main" tabindex="-1" class="flex-1 focus:outline-none">…</main>
</Layout>page is written to <body data-page>; the boot section at the end of src/scripts/main.ts uses it to
start page-specific features. Write internal links with url() from src/lib/site.ts
(url("/menu?diet=vegan")), so they keep working when the site is served from a sub-folder.
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, locations page, checkout and in the drawer, and every copy stays in sync.
checkPostcode() in main.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. The choice is saved (see Saved data) and shown in the
header.
When a postcode is checked, the element fires a zonechange event with the result in event.detail; the
locations page uses it to highlight the zone on the map. Press Enter in the postcode field to check.
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 are filled in the
browser, not at build time, because they depend on the time, and refresh every minute.
Menu filters and section nav#
src/pages/menu.astro renders every section from SECTIONS and every card with MenuCard.astro.
initMenu() in main.ts adds the behaviour: 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). /menu?item=margherita opens that item straight
away.
The section nav sticks under the header and highlights the section in view (an IntersectionObserver
scrollspy), 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()). A Large half Margherita / half Green Machine with extra nduja is £12.00 + £4.50 + £2.00 = £18.50. - 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 button, 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.
Promo codes#
PROMOS in src/data/shop.ts 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. 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.astro renders each card with its "Usually" price (dealWorth() of the default picks). The
deal dialog (openDeal() in main.ts) shows a select per choice and how much the deal saves against the
same items bought separately. Deal pizzas are classic crust with no extras.
The bundle builder on /deals lists Regular pizzas, sides, desserts and drinks from BUNDLE_SECTIONS
(rendered in deals.astro), 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 (initDeals()). Add bundle is enabled once a tier is unlocked.
Bundles appear in the cart as one line.
Checkout and time slots#
/checkout shows the empty state, 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. Nothing is sent: see Forms for where to connect a real order system.
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 for screen readers. Stages are in STAGES in main.ts; 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 on /locations is inline SVG in locations.astro. 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 (initLocations()). 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 and keep the data-map-zone ids in step with ZONES.
Catering calculator#
On /catering, 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#
Elements with .reveal fade in as they scroll into view; 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. A back-to-top button
appears after one screen; it lifts above toasts and the mobile order bar. To drop it, delete
src/scripts/back-to-top.ts, its import in Layout.astro and the .back-to-top styles in global.css.
Forms#
All forms validate in the browser with inline messages (fieldMessage() in main.ts) and show a success
state. None of them send anything until you connect them.
| Form | Where | Validates |
|---|---|---|
Newsletter (data-form="newsletter") |
Footer of every page (SiteFooter.astro) |
|
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) |
To connect the newsletter or catering form to a form service such as Formspree,
replace the successSwap(...) call in its setupValidation() callback in section 18 of main.ts:
$$<HTMLFormElement>('[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.';
});
});
});For real online ordering, the checkout's submit handler in initCheckout() 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. The site is static,
so that server is separate: a serverless function on your host, or an Astro server endpoint if you add an
adapter.
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 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 (ORDER_KEY, CART_KEY,
LAST_ORDER_KEY in main.ts) if you run several sites on one domain.
SEO and sharing#
Each page passes its title and description to Layout.astro, which writes the meta description,
canonical URL, Open Graph and Twitter tags. Canonical, og:url and og:image are absolute URLs built from
SITE_URL, so set it to your domain when you build:
SITE_URL=https://your-pizzeria.co.uk pnpm build(or change the default in astro.config.mjs). The 404 page passes noindex. The menu is in the built HTML,
so search engines see every item without running JavaScript. Add a sitemap with
@astrojs/sitemap and a
public/robots.txt. For local search, consider adding Restaurant structured data (JSON-LD) with each
shop's address, phone, openingHours and hasMenu; you can build it from STORES in the layout.
Accessibility#
- Skip link, landmarks (
header,nav,main,footer), oneh1per 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-expandedand closes on Esc. - The customiser and deal pickers use the native
<dialog>; the cart drawer isrole="dialog"witharia-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.
- Reduced motion turns off reveals, floating, spinning, card turns and tracker animations.
- Colours meet WCAG AA contrast. Keep text on
tomatoandbasilat 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 # static site in dist/
pnpm preview # preview dist/ locallydist/ is plain HTML, CSS and JavaScript. Pages are written as menu.html rather than
menu/index.html (build.format: "file" in astro.config.mjs), and links point to /menu without a
trailing slash. Netlify, Cloudflare Pages and GitHub Pages serve /menu from menu.html automatically;
Vercel does with cleanUrls (below).
- Netlify / Cloudflare Pages: build command
pnpm build, publish directorydist. - Vercel: import the repository; it detects Astro and static output needs no adapter. Add a
vercel.jsonwith{ "cleanUrls": true }so/menuservesmenu.html. - GitHub Pages: use the official
Astro GitHub Action. For a project site
(
username.github.io/repo), build withDEMO_BASE_PATH=/repo. - Any static host: upload the contents of
dist/. If the host doesn't map/menutomenu.html, changebuild.formatto"directory"inastro.config.mjs.
To serve the site from a sub-folder, set DEMO_BASE_PATH when building, e.g.
DEMO_BASE_PATH=/slice pnpm build. It becomes Astro's base, and every link written with url(),
including the links the script builds, includes it.
Tailwind finds class names in src/ automatically, including the ones inside strings in
src/scripts/main.ts, as long as they're written in full. demo/ (if present) is the build used for the
freeframe.dev demo; you can delete it.
TypeScript 7 setup#
package.json installs two TypeScript packages on purpose. @typescript/native is TypeScript 7 and
provides the tsc that pnpm typecheck runs. typescript is an alias for @typescript/typescript6,
the TypeScript 6 API that astro check and typescript-eslint load. When those tools support TypeScript
7, replace both with a single typescript@7. tsconfig.json extends Astro's strict preset.
Troubleshooting#
- Nothing happens when I click "Add". JavaScript isn't running: check the browser console, and run
pnpm checkafter editing the data (a missing comma or a wrong field name inMENUis the usual culprit). - A new class has no effect. Write class names in full (not built from pieces like
"bg-" + color) so Tailwind can find them. - "Open now" is wrong. Hours are in
TIMEZONE. CheckSTORES[].hours(Sunday first) and setTIMEZONEto your shop's zone, for example"America/New_York". - My postcode isn't recognised. Zones match UK postcode districts. For other countries, change
postcodeDistrict()andcheckPostcode()inmain.tsto read your format (for example a 5-digit ZIP) and list those codes inZONES[].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-cartto start fresh. - There are no time slots. The shop is closed for the next
SCHEDULE_DAYSdays, or the lead time runs past closing. Check the hours,pickupMinsanddeliveryMins. - Links or images break when served from a sub-folder. Build with
DEMO_BASE_PATH, and write internal links withurl()(in.astrofiles and insrc/scripts/main.ts) rather than plain"/..."strings. - Words run together after editing. Astro removes line breaks between text and a following inline
element. Keep the text and the element on one line, or add
{" "}.
License#
MIT, see the repository's license (included as LICENSE.txt in the download). Free for personal and
commercial use. All artwork (pizzas, food, map, logo and illustrations) is included and covered by the same
license.