Overview#
Crumb is a website for a neighborhood bakery and café with order-ahead pickup, built for Bramble & Rye, a fictional bakery on Phila Street in Saratoga Springs. It has a live "fresh out of the oven" board, a preorder catalog with per-day availability and quantity limits, a basket drawer with a pickup day, time slot and order cut-off, a pickup checkout with an order number and a calendar file, a celebration cake builder with a live drawing and price, a café menu with diet filters, an about page and a visit page with a wholesale form. This is the Nuxt version: Nuxt 4, Vue 3, TypeScript and Tailwind CSS v4, generated as a fully static site. The product cards, bake artwork and café menu 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/crumb.
Quick start#
Requirements: Node.js 24 or newer and pnpm (npm works too).
pnpm install
pnpm dev # http://localhost:3000Try an order: add a croissant and a seeded rye on the preorder page, pick a pickup slot in the basket and place the demo order at the checkout. Nothing is sent or charged.
The bake board, open-now badges and cut-off all follow the bakery's clock. To preview another time of day,
add ?now= to any page URL, for example http://localhost:3000/?now=2026-10-17T10:15.
pnpm generate writes the static site to .output/public/; pnpm preview serves it.
Pages#
| Route | File | Purpose | Notable sections and features |
|---|---|---|---|
/ |
app/pages/index.vue |
Home | Hero with the shop-window illustration, live bake board, how preordering works, this week's specials (from the product data), celebration cake teaser, café teaser, reviews, visit us with open-now badge, hours and map |
/shop |
app/pages/shop.vue |
Preorder | Shelf chips with counts (/shop?category=bread), "Picking up" day, "only that day's bakes" toggle, product cards grouped by shelf, add buttons and steppers, allergen note |
/cakes |
app/pages/cakes.vue |
Celebration cakes | Builder for size, flavor, filling, finish, color, message and pickup date; live SVG preview, price breakdown and allergens; recent cakes; cake FAQ |
/cafe |
app/pages/cafe.vue |
Café menu | Coffee, tea, brunch and sweet things with diet filters (/cafe?diet=vegan,gf), café notes |
/checkout |
app/pages/checkout.vue |
Checkout | Pickup day and slot, notes, contact details, pay at pickup or by card (test mode), order summary, confirmation with order number and .ics file, empty state with the last order |
/about |
app/pages/about.vue |
Our story | Story, timeline, the 36-hour sourdough process, suppliers, team, waste figures |
/visit |
app/pages/visit.vue |
Visit & wholesale | Illustrated map, hours with today highlighted, contact, getting here, wholesale inquiry form (/visit#wholesale), FAQ |
| any other | app/error.vue |
Not found (404) | Links back to home, preorder and café; generated as 404.html |
Project structure#
crumb/
app/
app.vue <NuxtLayout><NuxtPage /></NuxtLayout>, pages keyed so a link to the open page restarts it
error.vue 404 page
layouts/default.vue skip link, header, footer, basket drawer, toast and back to top
pages/ index, shop, cakes, cafe, checkout, about, visit (one .vue file per page)
components/
SiteHeader.vue top bar, header, navigation and mobile menu
BasketButton.vue the header's basket button with its count badge
SiteFooter.vue footer with address, hours and the bake list sign-up
OpenStatus.vue, CutoffText.vue, HoursList.vue the open-now badge, cut-off countdown and hours list
ProductCard.vue a bake's card and its week strip; CardAction.vue is its add button or stepper
DietTags.vue, QtyStepper.vue, LogoMark.vue, ArtSvg.vue
ShopCatalogue.vue shelf chips, pickup day, toggle and the grouped cards
BasketDrawer.vue the basket drawer; PickupFields.vue and ConflictAlert.vue are shared with the checkout
ToastBox.vue the toast
CakeBuilder.vue the cake builder; EarliestDate.vue is the earliest pickup date text
CafeMenu.vue diet chips, count and the menu
OrderSummary.vue, OrderDone.vue, IcsLink.vue the checkout summary, confirmation and calendar link
NewsletterForm.vue, WholesaleForm.vue, FieldError.vue, SuccessBox.vue forms
BackToTop.vue
composables/
useShop.ts shared browser state: clock, basket, drawer, toast and the basket actions
useValidation.ts inline form validation
usePageSeo.ts, useCurrentPage.ts
data/
shop.ts SHOP, TIMEZONE, LOCALE, CURRENCY, HOURS and PICKUP rules
bakes.ts CATEGORIES, ALLERGENS, DIETS, PRODUCTS, BAKE_SCHEDULE and CATEGORY_TINT
cafe.ts CAFE_MENU
cakes.ts CAKE: celebration cake options, lead time and limits
nav.ts header, mobile and footer links
utils/
clock.ts the shop clock: open-now, cut-off, pickup dates and slots, ?now=
board.ts the bake board's state, title and counts line
art.ts the SVG drawing code (bakes, cakes, tier icons, the paper bag)
bakeArt.ts bakeOf(id) / bakeInner(id) shortcuts for drawing a bake by id
basket.ts the saved basket: types, repair on read, conflicts, line info and totals
cake.ts cake configuration, price, summary, allergens and description
order.ts the last order and its .ics calendar file
validation.ts form field rules
format.ts money, lists and allergen text
safeStorage.ts, storage.ts localStorage with an in-memory fallback
plugins/
shop.client.ts starts the clock and reads the basket once the page has loaded
reveal.client.ts scroll reveals
same-page.client.ts links to the page already showing restart it, like a page load
stable-build.server.ts reproducible builds
router.options.ts query strings written like the HTML version
assets/css/main.css Tailwind v4 @theme tokens, component classes and @font-face rules
assets/fonts/ the woff2 files and their licenses
public/img/ favicon.svg, og-image.png and its source og-image.svg
nuxt.config.ts base path, head defaults, SITE_URL, prerendered routes, Tailwind
template.json gallery metadata (not part of your site)Customizing#
Brand and colors#
The design tokens are in the @theme block of app/assets/css/main.css. Every utility such as bg-bramble or
text-muted comes from them.
| Token | Default | Used for |
|---|---|---|
--color-flour |
#fbf5ec |
Page background |
--color-paper |
#fffdf8 |
Cards and form fields |
--color-oat |
#f3e7d5 |
Soft panels, bread tiles |
--color-line |
#e7d6bf |
Hairlines and dashed rules |
--color-ink |
#3a2620 |
Text, outlines, the footer |
--color-muted |
#6c574a |
Secondary text |
--color-bramble / --color-bramble-deep |
#74305a / #561f42 |
Brand color: buttons, links, basket |
--color-bramble-soft |
#f3dce7 |
Selected options |
--color-blush / --color-blush-deep |
#f7dcd3 / #a33a3f |
Cake tiles, sold out, error text |
--color-pistachio / --color-pistachio-deep |
#dfe8cb / #43602a |
Whole cake tiles, "on the shelf", vegan |
--color-butter / --color-butter-deep |
#f9e8b2 / #7a5a06 |
Pastry tiles, "baking now", badges |
--color-lilac, --color-sky |
#e5ddf0, #d9e8ee |
Pastel accents |
--color-crust |
#b9733c |
Decoration only (steam, bread brown) |
To change the brand color, edit the tokens:
@theme {
--color-bramble: #2f5d50;
--color-bramble-deep: #1f4237;
--color-bramble-soft: #d8ebe3;
}The illustrations use fixed colors in app/utils/art.ts and inline SVG in the pages, so a new brand color
doesn't recolor the bakes. The shelf tile colors are CATEGORY_TINT in app/data/bakes.ts.
Typography#
Three fonts are self-hosted with @font-face rules at the top of app/assets/css/main.css, from the same
woff2 files Google Fonts serves (in app/assets/fonts/, with their SIL Open Font License files):
| Token | Font | Used for |
|---|---|---|
--font-display |
Corben (400, 700) | Headings, prices, the wordmark |
--font-sans |
Karla (400–800, with italics) | Body text and UI |
--font-hand |
Delicious Handrawn | Hand-lettered notes (.hand), the cake message |
The rules keep the real family names, so the @theme tokens refer to "Karla", "Corben" and
"Delicious Handrawn" directly. To swap a font, put its woff2 files in app/assets/fonts/, change the paths
and family names in the @font-face rules and the matching token. The SVG artwork names
'Delicious Handrawn' and Corben directly for its labels.
Content#
Page text lives in the page files under app/pages/. Everything the interactive parts work from is in
app/data/:
| Export | File | What it holds |
|---|---|---|
SHOP |
shop.ts |
Name, address, phone and email used in the drawer, toasts and calendar file |
TIMEZONE, LOCALE, CURRENCY |
shop.ts |
America/New_York, en-US, USD |
HOURS |
shop.ts |
Opening hours per weekday, Sunday first, ["07:30", "16:00"] or null for closed |
PICKUP |
shop.ts |
cutoff ("14:00"), slotStep (30), lastSlotBeforeClose (30), daysAhead (14), notesMax (200) |
CATEGORIES |
bakes.ts |
The shelves: bread, pastry, bakes, whole |
ALLERGENS, DIETS |
bakes.ts |
Allergen names and diet labels (vegan, vegetarian, gf, df) |
PRODUCTS |
bakes.ts |
Every bake on the preorder page |
BAKE_SCHEDULE, BAKING_MINS |
bakes.ts |
When each bake comes out of the oven, and how long "baking now" shows before that |
CAFE_MENU |
cafe.ts |
Café sections and items |
CAKE |
cakes.ts |
Celebration cake options, the builder's default choices, lead time and limits |
Add a bake by adding an object to PRODUCTS:
{
id: "rye-bun",
name: "Caraway rye bun",
category: "bread",
price: 1.6,
unit: "each",
days: [5, 6],
max: 12,
blurb: "Soft rye buns with caraway, made for sandwiches.",
allergens: ["gluten"],
diet: ["vegan"],
art: { kind: "boule", crust: "#8c5532", top: "#a8693d" },
},| Field | Meaning |
|---|---|
id |
Unique, used in the basket and in data-add buttons |
category |
One of the CATEGORIES ids |
price |
In dollars; whole numbers show without cents ($58) |
days |
Weekdays it's baked (0 Sunday to 6 Saturday), or null for every day you're open |
max |
The most one order can hold |
leadDays |
Optional days' notice (default 1: order before the cut-off the day before) |
special |
Optional badge text; specials also appear on the home page |
allergens, diet |
Keys from ALLERGENS and DIETS |
art |
How it's drawn (see Images and artwork) |
TypeScript checks every field against the Product type. To show a new bake on the home page board, add it
to BAKE_SCHEDULE with the time the first tray comes out and, optionally, when it usually sells out:
{ id: "rye-bun", out: "09:15", soldOut: "13:00" },Café items are { name, price, desc, diet } inside a CAFE_MENU section. The cake builder reads
CAKE.sizes, CAKE.sponges, CAKE.fillings, CAKE.finishes and CAKE.colours; each option has an id, a
label and a price (a size has serves, factor and tiers, the diameters in inches used for the
drawing). CAKE.defaults sets the options picked when the builder opens.
The footer hours, reviews, the café teaser on the home page and the "Recently boxed up" cakes on /cakes are
written in the page files: update them alongside HOURS, CAFE_MENU and CAKE.
Images and artwork#
There are no photos. All artwork is SVG:
- Bakes are drawn by
bake(art)inapp/utils/art.tsfrom a product'sartobject.kindpicks the drawing:boule,tin,baguette,focaccia,croissant,painchoc,bun,knot,scone,tart,cookie,brownie,loafcakeorlayercake. Optional colors:crustandtop(breads),seeds/oats(tin),score: "leaf"(boule),almonds(croissant),dough/swirl(bun),sponge,frosting,fillingandtop("berries","walnuts","drip","flowers") forlayercake. A seeded random generator makes the same bake look the same every time, so the drawings made at build time match the ones the browser draws in the basket. - Celebration cakes are drawn by
cake(config)from the builder's choices; the recent cakes on/cakesand the cake on the home page use the same function with fixed choices. - The hero shop window, map, kitchen window, team avatars, coffee cup and 404 loaf are inline SVG in the page
files, drawn with the same code as the bakes. Edit them in place, or replace them with an
<img>frompublic/, with the base path in front::src="`${$config.app.baseURL}img/shop.jpg`".bakeOf(id)andbakeInner(id)inapp/utils/bakeArt.tsdraw a product's bake by id if you want to add more.
To use photos for the bakes, give each product an image path and replace the art <div> in
app/components/ProductCard.vue with an <img> (keep the 4:3 tile). Keep the hero image around 560×660 (portrait).
The social image is public/img/og-image.png (1200×630), rendered from og-image.svg. The favicon is
public/img/favicon.svg.
Navigation, header and footer#
The header, footer, basket drawer and toast are in app/layouts/default.vue, so they're shared by every page.
The links are in app/data/nav.ts: NAV is the main navigation, MOBILE_NAV the mobile menu (with "Home"
in front) and FOOTER_NAV the footer's "Explore" links. The current page is marked with aria-current="page"
(useCurrentPage()). To add a page, create app/pages/<route>.vue, add it to NAV and to routes in
nuxt.config.ts so it is generated.
Features#
Shop clock and opening hours#
All times use the bakery's clock (TIMEZONE) via shopNow() in app/utils/clock.ts, so a visitor abroad sees
the shop's real state. useShop() (app/composables/useShop.ts) shares one clock (state.now) between every
component and checks it every 30 seconds. It starts once the first page has loaded
(app/plugins/shop.client.ts): the generated HTML shows the static text ("Open Tuesday to Sunday", "Order by
2pm for tomorrow"), which avoids hydration mismatches, and the browser then switches to live text.
OpenStatus.vueshows "Open now until 4pm", "Closing soon, at 4pm" or "Closed · opens Tuesday 7:30am" with a colored dot (shopStatus()).CutoffText.vueshows the countdown toPICKUP.cutoff("Order in the next 3h 40m for pickup tomorrow"), or when preorders open next (cutoffText()).HoursList.vuelists the week from Tuesday with today in bold and a "Today" badge.- The copyright year in the footer follows the shop's clock too.
Add ?now=YYYY-MM-DDTHH:MM to a URL to test any time.
Bake board#
The board on the home page (app/pages/index.vue, worked out by boardState() in app/utils/board.ts) shows
every BAKE_SCHEDULE entry baked today. Each bake is Out at 7:30am until BAKING_MINS before its time,
then Baking now (with rising steam), then Out since 7:30am, and Sold out (faded) after its optional
soldOut time. Bakes on the shelf come first and sold-out ones last. The heading shows the day and a counts
line ("8 on the shelf · 1 in the oven · 2 still to come"). After closing (and on closed days) the board shows
the next bake day instead, so the page never looks empty. Each bake links to its shelf on the preorder page.
Preorder catalog#
ShopCatalogue.vue groups PRODUCTS by shelf. The generated page has every card; in the browser the chips
filter by shelf and keep the address in step (?category=pastry, written with history.replaceState), the
"Picking up" select is the basket's pickup day, and "Only that day's bakes" hides anything not baked then.
Cards (ProductCard.vue) show the bake days as a Monday-first week strip, diet tags and allergens, and
CardAction.vue shows:
- Add to basket when it fits the chosen day.
- Add for Sat, Oct 17 when it doesn't: adding it moves the pickup to the first day that suits the whole basket, and a toast says so.
- A quantity stepper and "In your basket" once it's in the basket, capped at
max.
Filters are buttons with aria-pressed, and the result count is a live region. A link to the preorder page
from a filtered shelf (the footer's "Preorder for pickup") shows every bake again, like a page load
(app/plugins/same-page.client.ts).
Basket drawer#
The basket button in the header opens BasketDrawer.vue (#basket-drawer), a modal dialog with a focus trap;
Esc, the close button or the backdrop close it and focus returns to the button that opened it. It lists each
line with a stepper (at one, the minus becomes a bin) and a remove button, then the pickup details
(PickupFields.vue and ConflictAlert.vue, shared with the checkout):
- Pickup day: open days from the earliest possible (
earliestDate()) up toPICKUP.daysAhead(CAKE.daysAheadwhen there's a celebration cake). Orders beforePICKUP.cutoffcan pick up the next open day; after it, the day after. Whole cakes add theirleadDays, celebration cakesCAKE.leadDays. Days that don't suit everything say "(2 items not baked)". - Time slot: half-hour slots from opening time until
lastSlotBeforeCloseminutes before closing. - Conflicts: if something isn't baked on the chosen day, the drawer says so, offers the next day that suits everything, and disables checkout until it's fixed.
- Notes up to
PICKUP.notesMaxcharacters, saved as you type.
The basket actions (addProduct(), setQty(), removeLine(), setPickup(), setNotes()), the drawer and
the toast (ToastBox.vue) live in useShop().
Celebration cake builder#
CakeBuilder.vue builds the radio cards from CAKE and redraws the cake and price on every change; the
drawing has a text description of the cake for screen readers. The price is the size's price plus flavor,
filling and finish extras multiplied by the size's factor, rounded to whole dollars (cakePrice() in
app/utils/cake.ts). The color swatches are disabled for the naked finish. The message is limited to
CAKE.messageMax characters with a live counter. The date's minimum is CAKE.leadDays days ahead (one more
after the cut-off) and closed days are rejected. Adding the cake puts it in the basket, sets the pickup to that
date and shows a toast; one order holds up to CAKE.maxPerOrder cakes. On phones a second "Add cake to
basket" button with the price sits at the end of the form.
Café menu filters#
CafeMenu.vue renders CAFE_MENU. The diet chips combine (a dish must match all of them; vegan counts as
vegetarian and dairy-free), keep the address in step (?diet=vegan,gf) and show an empty state with a clear
button when nothing matches.
Checkout and calendar file#
app/pages/checkout.vue shows the pickup picker, notes, contact fields, payment choice and the order summary
(OrderSummary.vue). "Pay now by card" reveals a card form in test mode (Luhn-checked number in groups of
four, MM / YY expiration, CVC). If something isn't baked on the chosen day, placing the order is refused
until the day is fixed. On a valid submit it creates an order number (BR- and five digits), saves the order to
crumb-last-order, empties the basket and shows a confirmation (OrderDone.vue) with an Add to calendar
(.ics) link (IcsLink.vue) built by icsFor() in app/utils/order.ts: a 30-minute event at the pickup slot
with TZID=America/New_York and a VTIMEZONE block, with the items in the description. The confirmation says
a confirmation email is on its way and gives the shop phone number (SHOP.phone) for changes, quoting the
order number. The empty checkout shows the last order and its calendar link.
Scroll reveal, motion and back to top#
Elements with the reveal class fade in as they scroll into view (app/plugins/reveal.client.ts); the steam
and the "baking now" pulse are CSS animations in main.css. All of them are off with
prefers-reduced-motion. A back-to-top button (BackToTop.vue) appears after one screen of scrolling and
moves focus to the main content; remove it from app/layouts/default.vue and the .back-to-top styles in
main.css to drop it.
Forms#
All forms validate in the browser with inline messages (rules in app/utils/validation.ts, wiring in
app/composables/useValidation.ts) and show a success state. None of them send anything until you
connect them. The wholesale confirmation shows a short reference (BR- and four characters, from makeRef() in
WholesaleForm.vue) and says Ruth replies within two business days; the bake list confirmation asks
subscribers to look out for a confirmation email. Change the wording to match how you actually handle
inquiries, and show your form service's own reference if it returns one.
| Form | Where | Validates |
|---|---|---|
Bake list sign-up (data-form="newsletter") |
app/components/NewsletterForm.vue, footer of every page |
|
Wholesale inquiry (data-form="wholesale") |
app/components/WholesaleForm.vue, /visit |
Business, type, name, email, optional phone (10–13 digits), loaves a day (a whole number from 10 to 400), at least one interest, start date at least 7 days ahead, message up to 600 characters |
Cake builder (#cake-form) |
app/components/CakeBuilder.vue, /cakes |
Pickup date (lead time, open day, up to CAKE.daysAhead), message length |
Checkout (#checkout-form) |
app/pages/checkout.vue, /checkout |
Time slot, name, email, phone (10–13 digits), allergen confirmation, card fields when paying now |
Rules come from attributes on each field: required and data-label ("Your email is required."), the input
type, data-validate (name, card, expiry, cvc, open-day), min / max, maxlength,
data-min-msg and, for checkboxes, data-msg. A field gets them with v-bind="v.field(id)"; errors appear
in <FieldError> (<p id="<field id>-error">) after the field, linked with aria-describedby and
aria-invalid, and the first problem is focused.
To connect the wholesale form to a form service such as Formspree, post the data in
onSubmit in WholesaleForm.vue before showing the thank-you message:
async function onSubmit(e: Event) {
const form = e.currentTarget as HTMLFormElement;
const ok = v.validate(form);
status.value = ok ? "" : "Please check the highlighted fields.";
if (!ok) return;
const res = await fetch("https://formspree.io/f/your-id", {
method: "POST",
body: new FormData(form),
headers: { Accept: "application/json" },
});
if (!res.ok) return (status.value = "Sorry, that didn’t send. Please try again or call us.");
const value = (name: string) => (form.elements.namedItem(name) as HTMLInputElement).value.trim();
sent.value = { first: value("name").split(/\s+/)[0]!, business: value("business"), ref: makeRef("BR") };
}The newsletter works the same way in NewsletterForm.vue. Every field already has a name. If you build with
pnpm build (a Node.js server, see Building and deploying), you can post to your
own server route instead, for example server/api/wholesale.post.ts.
For real orders, onSubmit in app/pages/checkout.vue builds an order object with the lines, pickup day
and slot, contact details and payment choice; send it to your order system where the comment says so, take
payment with your provider (Stripe Checkout, Square…) and only show the confirmation when it succeeds. Remove
the test-mode card form and its "Test mode" note when you do, and never trust prices from the browser:
recalculate them on the server from your product data.
Saved data (localStorage)#
| Key | Shape | Notes |
|---|---|---|
crumb-basket |
{ lines, date, time, notes } |
lines are { id, qty } or { id: "cake", key, cake: { size, sponge, filling, finish, colour, message }, qty: 1 }; date is YYYY-MM-DD, time is HH:MM |
crumb-last-order |
{ number, placedAt, date, time, notes, name, email, phone, payment, lines, total } |
Written at checkout; shown on the empty checkout page |
Everything is checked on every read (loadBasket() in app/utils/basket.ts, readLastOrder() in
app/utils/order.ts): unknown products and cake options are dropped, quantities become whole numbers within
max, duplicates merge, more than CAKE.maxPerOrder cakes are dropped, dates outside the bookable range and
slots that don't exist reset, and the repaired value is written back when a page loads. Prices are never
stored; they always come from PRODUCTS and CAKE. Components read the basket from useShop() (state.basket), which
is empty while the page is generated and hydrated, and other open tabs update through the storage event. When
the browser blocks storage (a sandboxed frame, strict privacy settings), safeStorage in
app/utils/safeStorage.ts keeps the basket in memory for the page view. To reset, run localStorage.removeItem("crumb-basket") in the console,
or clear the site's data. Change the key names if you run several sites on one domain.
SEO and sharing#
- Each page calls
usePageSeo({ title, description })(app/composables/usePageSeo.ts), which sets the title, description, canonical link, Open Graph tags (with the imageimg/og-image.png) and the Twitter card. The 404 page passesnoindex: true. Site-wide tags (theme color, favicon) are inapp.headinnuxt.config.ts. - Canonical,
og:urlandog:imageare absolute:SITE_URLplus the base path. Build with your domain so social images get absolute URLs (the default ishttps://brambleandrye.example):
SITE_URL=https://www.your-bakery.example pnpm generateThe product cards and café menu are in the generated HTML, so every search engine sees them. Consider adding a
public/robots.txt, a sitemap (leave out the checkout) and a Bakery structured
data block as JSON-LD with the address, telephone and openingHours.
Accessibility#
- Skip link, landmarks (
header,nav,main,footer), oneh1per page and a logical heading order. - Visible focus outlines in the brand color, including radio cards, chips and swatches; WCAG AA contrast.
- The mobile menu button has
aria-expandedand an "Open menu" / "Close menu" label, and closes on Esc. - The basket drawer is
role="dialog"witharia-modal, a focus trap, Esc and the backdrop to close, and focus returned to the button that opened it. - Shelf and diet chips are toggle buttons (
aria-pressed); the result counts, toasts and cake price are live regions. - Steppers have labeled buttons ("One more Butter croissant"), remove buttons include the item name, and the week strip has a text version ("Baked Thursdays & Saturdays.").
- The cake drawing has a text description; decorative art is hidden from screen readers.
- Form errors are linked with
aria-describedby, fields getaria-invalidand the first error is focused. - Reduced motion turns off reveals, steam, the "baking now" pulse and smooth scrolling.
When editing, keep exactly one h1 per page, give every new input a <label>, and write full class names
(Tailwind only finds classes it can read in the source).
Building and deploying#
pnpm generate # static site in .output/public/
pnpm preview # serve it locallyDeploy .output/public/ to any static host (Netlify, Cloudflare Pages, GitHub Pages, S3) or import the
repository into Vercel or Netlify, which detect Nuxt. Pages are written as shop.html, checkout.html and so
on; most static hosts serve /shop from shop.html automatically, and 404.html is the not-found page.
For a site in a sub-folder, set DEMO_BASE_PATH when building (DEMO_BASE_PATH=/crumb pnpm generate): it
becomes app.baseURL in nuxt.config.ts. pnpm build:demo does this for the freeframe.dev demo and writes
demo/, which you can delete.
Need server features (an order API, payments)? pnpm build makes a Node.js server build in .output/
(pages are still prerendered); add routes in server/api/ and deploy to a Node-capable host.
pnpm lint runs ESLint 10 with @nuxt/eslint. tsconfig.json only references the configs Nuxt writes to
.nuxt/, so pnpm typecheck checks the app and config with Nuxt's strict settings. typescript is aliased to
@typescript/typescript6, the TypeScript 6 API that vue-tsc needs.
Troubleshooting#
- The bake board says "Tomorrow" all day.
HOURSdecides when you're open; check the closing time for today and thatTIMEZONEis your bakery's zone. - A bake never appears on the board. It needs a
BAKE_SCHEDULEentry and must be baked that day (days). - Customers can't pick tomorrow. It's after
PICKUP.cutoff, tomorrow is closed, or something in the basket needs more notice (leadDays). - The basket shows old items. Stored baskets are repaired against
PRODUCTS; if you renamed anid, old baskets drop that line. Clearcrumb-basketto start fresh. - A new class has no effect. Tailwind only finds full class names in the source; write
bg-pistachio, not"bg-" + colour. - Images or links break under a sub-folder. Build with
DEMO_BASE_PATHand reference public files with the base path (`${$config.app.baseURL}img/og-image.png`);<NuxtLink>adds it automatically. - Social previews point to
brambleandrye.example. SetSITE_URLwhen building. - A new page gives a 404 after
pnpm generate. Add its route toroutesinnuxt.config.ts. - The calendar file has the wrong time. Events use
TIMEZONE; if you change it, update theVTIMEZONErules inicsFor()too.
License#
Freeframe Commercial License: use it in unlimited websites and apps for yourself and your clients; it may not
be resold or redistributed as a template. The full terms, with your licensee details, are in LICENSE.txt and
at https://freeframe.dev/license#commercial. Third-party components keep their own licenses. Karla, Corben and
Delicious Handrawn are licensed under the SIL Open Font License (app/assets/fonts/OFL-karla.txt,
app/assets/fonts/OFL-corben.txt, app/assets/fonts/OFL-delicious-handrawn.txt). Bramble & Rye, its team and
its customers are fictional.