Freeframe

Documentation

Crumb

v1.0.0Updated Nov 12, 2026A charming bakery and café website with order-ahead pickup, a live bake board and a celebration cake builder.

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

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

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

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

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

ts
{ 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) in app/utils/art.ts from a product's art object. kind picks the drawing: boule, tin, baguette, focaccia, croissant, painchoc, bun, knot, scone, tart, cookie, brownie, loafcake or layercake. Optional colors: crust and top (breads), seeds / oats (tin), score: "leaf" (boule), almonds (croissant), dough / swirl (bun), sponge, frosting, filling and top ("berries", "walnuts", "drip", "flowers") for layercake. 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 /cakes and 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> from public/, with the base path in front: :src="`${$config.app.baseURL}img/shop.jpg`". bakeOf(id) and bakeInner(id) in app/utils/bakeArt.ts draw 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.

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.vue shows "Open now until 4pm", "Closing soon, at 4pm" or "Closed · opens Tuesday 7:30am" with a colored dot (shopStatus()).
  • CutoffText.vue shows the countdown to PICKUP.cutoff ("Order in the next 3h 40m for pickup tomorrow"), or when preorders open next (cutoffText()).
  • HoursList.vue lists 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 to PICKUP.daysAhead (CAKE.daysAhead when there's a celebration cake). Orders before PICKUP.cutoff can pick up the next open day; after it, the day after. Whole cakes add their leadDays, celebration cakes CAKE.leadDays. Days that don't suit everything say "(2 items not baked)".
  • Time slot: half-hour slots from opening time until lastSlotBeforeClose minutes 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.notesMax characters, 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 Email
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:

ts
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 image img/og-image.png) and the Twitter card. The 404 page passes noindex: true. Site-wide tags (theme color, favicon) are in app.head in nuxt.config.ts.
  • Canonical, og:url and og:image are absolute: SITE_URL plus the base path. Build with your domain so social images get absolute URLs (the default is https://brambleandrye.example):
sh
SITE_URL=https://www.your-bakery.example pnpm generate

The 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), one h1 per 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-expanded and an "Open menu" / "Close menu" label, and closes on Esc.
  • The basket drawer is role="dialog" with aria-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 get aria-invalid and 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#

sh
pnpm generate     # static site in .output/public/
pnpm preview      # serve it locally

Deploy .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. HOURS decides when you're open; check the closing time for today and that TIMEZONE is your bakery's zone.
  • A bake never appears on the board. It needs a BAKE_SCHEDULE entry 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 an id, old baskets drop that line. Clear crumb-basket to 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_PATH and reference public files with the base path (`${$config.app.baseURL}img/og-image.png`); <NuxtLink> adds it automatically.
  • Social previews point to brambleandrye.example. Set SITE_URL when building.
  • A new page gives a 404 after pnpm generate. Add its route to routes in nuxt.config.ts.
  • The calendar file has the wrong time. Events use TIMEZONE; if you change it, update the VTIMEZONE rules in icsFor() 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.

Search documentation

Search the guides and every template's documentation