Freeframe

Documentation

Sprocket

v1.0.0Updated Oct 31, 2026A bold product launch website for a hardware startup taking preorders, with a live bike configurator, a range estimator and a refundable-deposit reservation flow.

Overview#

Sprocket is a multi-page product launch and preorder website template for a hardware startup. The demo business is Bonfield Bikes, a fictional e-bike maker in Duluth, Minnesota, taking reservations for the Portage, a compact cargo e-bike built for hills and winter. It is built with Next.js 16 (App Router), React 19, TypeScript and Tailwind CSS v4, and exported as a fully static site. This is version 1.0.0 of the Next.js stack. See the template page at freeframe.dev/templates/sprocket, which links to the live demo.

The template has a product configurator with an SVG bike that recolors and shows each accessory as you choose, shareable build links and a saved build, a range estimator, production batches whose ship month and spots left roll forward on their own, a reservation flow with a refundable deposit and a saved reservation, a car vs e-bike cost calculator, a build log with articles, a team and careers page, and a support page with FAQ search, delivery zones and a contact form. Every date is worked out in America/Chicago (Central time), whatever the visitor's own time zone is.

Quick start#

Requirements: Node.js 24 and pnpm (corepack enable pnpm) or npm.

sh
pnpm install
pnpm dev          # http://localhost:3000
Command What it does
pnpm dev Development server at http://localhost:3000
pnpm build Static site in out/
pnpm start Serve out/ locally (npx serve out)
pnpm lint ESLint (Next.js core-web-vitals and TypeScript rules)
pnpm typecheck Generate route types (next typegen), then type-check with tsc (TypeScript 7)
pnpm build:demo Build for /demos/nextjs/sprocket into demo/ (the freeframe.dev demo; you can delete it)

Pages#

Route Purpose Notable sections and features
/ (src/app/page.tsx) Home Hero with the Portage drawing and color swatches (shows your saved build), "Reserve yours" and "Build yours" links carrying the build, the open batch with spots left and a meter, owner numbers, feature highlights, press quotes, FAQ teaser, call to action
/bike Configurator Frame color, battery and accessories rendered from BUILD, live drawing, price breakdown, "Reserve this build", "Save this build", "Copy link", saved build panel; what every Portage includes
/range Range estimator Presets, battery, rider and cargo weight, terrain, temperature, assist level, studded tires and round trip; result with a band, bars for both batteries, the working and a battery suggestion
/reserve Reserve Build summary from the URL or saved build, delivery zone from the ZIP code, details form with validation, success state with a reference, saved reservation with a two-step cancel
/specs Specs Key figures, full specifications, footprint drawing, comparison table, car vs Portage cost calculator
/journal Build log Category filter (?cat=), the latest post featured, the rest in a grid, each with a drawn cover
/article One post (?a=<slug>) Title, category, date, cover, body, author with portrait, batch call to action, three more posts; unknown slugs show the latest post with a notice
/about About Story with a hill map, timeline, seven illustrated team portraits, backers, open roles with a team filter (?team=, #careers)
/support Support FAQ with search, warranty, delivery zones with a ZIP checker, contact form with success state (?topic=)
src/app/not-found.tsx Not found A wheel rolling down the hill and links to the main pages

Project structure#

sprocket/
  src/
    app/
      layout.tsx           html shell: fonts, metadata, skip link, header, footer, reveal, back to top
      page.tsx             home page; bike/, range/, reserve/, specs/, journal/, article/, about/, support/
      not-found.tsx        the 404 page
      fonts.ts             Kanit and Encode Sans with next/font/local
      globals.css          Tailwind CSS v4: @theme tokens, a few utilities and components
      icon.svg             favicon (the sprocket mark)
    components/
      SiteHeader.tsx       batch bar, logo, main menu and mobile menu
      SiteFooter.tsx       footer links, address, hours and copyright year
      live.tsx             BatchText, BatchMeter and YearSpan: text worked out from today
      BikeArt.tsx          the Portage drawing, colored and fitted from a build
      Swatches.tsx         the frame color radio group (home hero and configurator)
      HomeHero.tsx         home page hero with the swatches and build links
      Configurator.tsx     /bike: options, price, save, load, forget, copy link
      PriceLines.tsx       price breakdown lines (configurator and reserve)
      RangeEstimator.tsx   /range
      Reserve.tsx          /reserve: summary, zones, form, success, saved reservation
      CostCalculator.tsx   the car vs Portage calculator on /specs
      JournalList.tsx      /journal with the category filter
      ArticleView.tsx      /article?a=<slug>
      PostCard.tsx         a post card in the journal and under articles
      art.tsx              article covers (Cover) and team portraits (Portrait)
      RolesList.tsx        open roles with the team filter on /about
      FaqSearch.tsx        the FAQ and its search on /support
      ZoneChecker.tsx      ZIP checker and delivery zone list on /support
      ContactForm.tsx      the contact form on /support
      RevealController.tsx scroll reveals
      BackToTop.tsx        back-to-top button
      icons.tsx            logo and phone icon
    data/site.ts           all content and settings: COMPANY, BUILD, BATCHES, ZONES, RANGE, COST, DEMO, POSTS, PEOPLE, ROLES
    lib/
      sprocket.ts          dates in Central time, batches, builds and prices, range, zones, references, journal dates
      client.ts            browser hooks: useSearch, useNow, useToday, useBatch, replaceQuery, useDocumentTitle
      store.ts             saved build and reservation (validated reads, hooks)
      safe-storage.ts      localStorage with an in-memory fallback
      metadata.ts          per-page title, description and Open Graph
      asset.ts             adds the base path to files in public/
    fonts/                 Kanit and Encode Sans woff2 files with their OFL licenses
  public/img/              og-image.svg and og-image.png (1200×630), favicon.svg
  next.config.ts           static export, optional base path
  package.json             scripts and dependencies
  README.md, DOCS.md, CHANGELOG.md

Customizing#

Brand and colors#

The design tokens are in the @theme block of src/app/globals.css. Tailwind turns each --color-* token into utilities (bg-ore, text-lake, border-ink…).

Token Value Used for
--color-snow, --color-snow-2 #f3f2ed, #e8e6de page background, sunken bands
--color-card #ffffff cards and forms
--color-ink, --color-ink-2, --color-ink-3 #14171a, #1e2327, #30373c text, dark bands, borders on dark
--color-muted, --color-frost #575c5f, #c9d3d6 secondary text on light and on dark
--color-ore, --color-ore-deep, --color-ore-soft #c63d1f, #a8301a, #f7dbd2 Taconite red: buttons, links, highlights
--color-ember #ff8a66 the red on dark backgrounds
--color-lake, --color-lake-deep, --color-lake-soft, --color-ice #0f6672, #0b4e57, #d4e8ea, #9fd9e3 Harbor teal accents
--color-ok, --color-bad #2d6a3e, #a8301a success and error text

To change the brand red, edit --color-ore and its partners (Next.js rebuilds the CSS on save), and update the few hex values written into SVG art (the logo in src/components/icons.tsx, src/app/icon.svg, the hero backdrop in src/components/HomeHero.tsx):

css
@theme {
  --color-ore: #1d5fbf;
  --color-ore-deep: #164a96;
  --color-ore-soft: #d8e5f7;
}

The bike's frame color is not a token: it comes from the chosen color in BUILD.colors (see Configurator). The repeated pieces (.btn-ore, .card, .chip, .option, .swatch, .tread dividers and so on) are in @layer components in the same file.

Typography#

Two fonts, self-hosted from src/fonts/ with next/font/local in src/app/fonts.ts (the same Latin files Google Fonts serves, so lines wrap exactly like the HTML version):

  • Kanit (500, 600, 700, 800 and italic 700, 800) for headlines (heavy italic uppercase through the headline utility), buttons, labels and numbers: font-display.
  • Encode Sans (variable, weights 400–700) for body text: font-sans.

Each face is declared under its plain family name (declarations sets font-family), with fallback set to that name and adjustFontFallback: false, so --font-display and --font-sans in @theme can say "Kanit" and "Encode Sans" directly. Figures use tabular, lining numerals through the num utility.

To swap a font, put its woff2 files in src/fonts/, change src, the font-family declaration and fallback in fonts.ts, and the family name in @theme. public/img/og-image.svg names the fonts too.

Content#

Page copy lives in the page files under src/app/ and the components. Everything that is listed, priced or calculated lives in src/data/site.ts:

Constant What it holds
COMPANY name, phone, email, address, timeZone (America/Chicago), reference prefixes (BB, BS)
BUILD basePrice (3895), deposit (150), defaults, colors, batteries, accessories
BATCHES firstShip: "2026-03", every: 2, lead: 3, size: 120, minLeft: 9
ZONES delivery zones by ZIP prefix, with fees
RANGE the range formula, defaults, assist levels, terrain and presets
COST the cost calculator's electricity, upkeep and years
DEMO KEEP_DEMO_CURRENT and DEMO_ANCHOR for journal dates
CATEGORIES, POSTS the build log
PEOPLE the team, with portrait values
TEAMS, ROLES open roles

Adding an accessory is one entry; it appears in the configurator, the price, the reserve summary and saved builds. The key is what goes into the URL (?acc=lock):

ts
lock: { name: "Frame lock & chain", price: 95, detail: "A keyed-alike chain that plugs into the frame lock." },

To draw it on the bike, add a <g {...part("lock")}>…</g> group to src/components/BikeArt.tsx; it shows only when the build has that accessory.

Adding a post: add an object at the top of POSTS (newest first) with a unique slug, a cat from CATEGORIES, an author from PEOPLE, a date (YYYY-MM-DD), minutes, an art key from ART in src/components/art.tsx and a body made of ["p", text], ["h2", text], ["ul", [items]] and ["quote", text, who] blocks.

The FAQ on /support is the FAQS array in src/components/FaqSearch.tsx: each entry has the question q, the answer a (text or JSX) and text, the answer as plain text for the search.

Images and artwork#

All artwork is SVG drawn for this template, no photos:

  • The Portage is src/components/BikeArt.tsx (viewBox 800×500), used on the home page, the configurator and the reserve page. The frame strokes have the class frame and take their color from the --frame custom property; each optional part is a group with data-part (rack, kidseat, panniers, basket, studs, pogies, battery-dual). To use photos instead, render one image per color in BikeArt and keep a 16:10 frame.
  • Article covers are the ART drawings in src/components/art.tsx (640×400); <Cover art={p.art} /> picks one. Replace them with images of the same ratio if you like.
  • Team portraits are drawn by <Portrait p={person} /> from the skin, hair, style (short, long, bun, bald, curly), shirt, glasses and beard values in PEOPLE (square).
  • The hill map (about), zone rings (support), footprint chart (specs), feature art (home) and the 404 drawing are inline in their pages.
  • The social image is public/img/og-image.svg, exported to og-image.png (1200×630). The favicon is src/app/icon.svg.

Files in public/ used in markup go through asset() (src/lib/asset.ts) so they keep working under a base path.

The header is src/components/SiteHeader.tsx: the NAV array at the top feeds both the main menu and the mobile menu, and marks the current page with aria-current="page" (also lists other routes that count, such as /article for the journal). The footer is src/components/SiteFooter.tsx. To add a page, create src/app/<route>/page.tsx with export const metadata = pageMetadata(title, description), then add it to NAV and the footer.

The thin bar above the header (data-batch-line) shows the open batch on every page; delete the batch-bar block in SiteHeader.tsx to remove it.

Features#

Dates and the static build#

The static export can't know today's date or the URL, so anything that depends on them is worked out in the browser right after the page loads. Until then the page shows the text it was written with (the batch of October 31, 2026, the standard Portage, the latest post), so search engines and people without JavaScript see a complete page. The hooks in src/lib/client.ts and src/lib/store.ts use useSyncExternalStore and return null (or the written values) while prerendering, so there is no hydration mismatch. The configurator, range estimator and reserve form start a fresh copy of themselves once the browser has the URL and saved data (key="live"), the same way the HTML version reads them on load.

Production batches#

batchFor(dateKey) in src/lib/sprocket.ts works out the batch open on a date from BATCHES:

  • Batch 1 ships in firstShip (March 2026) and a batch ships every every (2) months.
  • The open batch is the first one shipping at least lead (3) months after the current month, so each batch takes reservations for two calendar months and closes on the last day of the second.
  • Spots left fall in a straight line from size (120) on the first day of that window to about minLeft (9) on the last: max(minLeft, round(size − (size − minLeft) × elapsedDays ÷ windowDays)).

On October 31, 2026 (Central time) that gives Batch 6, ships January 2027, 11 of 120 spots left (109 reserved), closes Saturday, October 31; Batch 7 opens November 1 and ships March 2027. On November 1, 2026 it is Batch 7 with 120 left. A year later (October 31, 2027) it is Batch 12, shipping January 2028, 11 left.

useBatch() returns today's batch, and <BatchText field="…" /> in src/components/live.tsx prints one fact about it in an element with the matching data-batch-* attribute: name ("Batch 6"), ship ("January 2027"), left, size, taken, close ("Saturday, October 31") and next; <BatchMeter /> is the reserved bar. For a real launch with fixed dates, replace batchFor() with your own table of batches.

Configurator#

/bike, in src/components/Configurator.tsx: the options are rendered from BUILD into [data-colors] (an ARIA radio group of swatches: arrow keys move and select), [data-batteries] (radios) and [data-accessories] (checkboxes).

  • Price: basePrice $3,895 + battery (standard included, dual +$520) + accessories (rear rack $180, kid seat $240, panniers $160, front basket $135, studded tires $170, pogies $65). Example: Harbor Teal, dual battery and kid seat = $3,895 + $520 + $180 (rack, added automatically) + $240 = $4,835.
  • Dependencies: accessories with requires: ["rack"] (kid seat, panniers) add the rear rack, with the note "Rear rack & running boards added: the kid seat mounts on it."; removing the rack removes them ("Kid seat removed too: it mounts on the rear rack.").
  • Drawing: <BikeArt build={build} /> sets --frame, shows the matching data-part groups (the dual battery shows battery-dual) and describes the build in the SVG's aria-label.
  • URL: every change updates the address with history.replaceState: /bike?color=harbor&battery=dual&acc=rack,kidseat (a ?now= preview is kept). "Reserve this build" links to /reserve with the same parameters. "Copy link" copies the address, or shows it in a field to copy when the clipboard isn't available.
  • Saved build: "Save this build" stores it under sprocket:build; the panel below then shows "Your saved build: Harbor Teal · Dual battery · 2 accessories · $4,835" with "Load it" and "Forget it". On a later visit without parameters the saved build is loaded. The home page hero shows the saved build too.

Values from the URL and storage go through normalizeBuild(): unknown colors and batteries fall back to the defaults, unknown accessories are dropped, and every key is checked with Object.prototype.hasOwnProperty.

Home hero#

src/components/HomeHero.tsx: the swatches under the hero bike ([data-hero-swatches]) recolor it and update the "Reserve yours" and "Build yours" links (data-hero-reserve, data-hero-build) so the chosen color carries over.

Range estimator#

/range, in src/components/RangeEstimator.tsx, with the formula in RANGE and rangeMiles():

miles = battery Wh × 0.9 × temperature factor ÷ (assist Wh/mi × terrain factor × weight factor × studs)
temperature factor = 1 at 60°F and above, else max(0.55, 1 − (60 − °F) × 0.0065)
weight factor      = 1 + (rider and cargo lb − 200) ÷ 500
assist Wh/mi       = Eco 9, Tour 13, Sport 18, Turbo 24
terrain factor     = Mostly flat 1, Rolling 1.18, Steep hills 1.42;  studded tires × 1.08

The result shows the miles for the chosen battery (rounded), a band of ±12%, bars for both batteries with a dashed line at the round trip × 1.25, the working, and a suggestion: the standard battery if it covers the round trip × 1.25, else the dual battery, else "Dual battery, and a charge at work". The suggestion links to /bike?battery=<key>. ?battery=dual on /range preselects a battery.

Inputs Standard Band Dual Suggestion
Defaults: 220 lb, Rolling, 50°F, Tour, no studs, 12 mi 33 mi 29–37 47 mi standard (36% of a charge)
January commute: 230 lb, hills, 5°F, Sport, studs, 14 mi 12 mi 11–14 18 mi dual (113% / 79%)
School run: 290 lb, Rolling, 45°F, Tour, 6 mi 28 mi 25–31 40 mi standard
Summer grocery run: 260 lb, flat, 75°F, Eco, 8 mi 56 mi 49–62 80 mi standard

The round trip must be 1–80 miles; otherwise an inline error shows and the last valid value is kept.

Reservation#

/reserve, in src/components/Reserve.tsx:

  • Build: from the URL parameters, else the saved build, else the default; the line under the drawing says which ("Your build from the configurator.", "Your saved build.", "The standard Portage. Build your own to change it."). "Change" links back to the configurator with the build.
  • Delivery zone: typing a ZIP code looks up ZONES by its first three digits (zoneFor()): 556–558 and 548 are Duluth–Superior hand delivery (free), most of Minnesota, Wisconsin, Iowa, the Dakotas and the Upper Peninsula are Upper Midwest freight ($149), the rest of 010–994 is crated freight ($249), and Alaska, Hawaii and the territories (995–999, 967–968, 000–009) can't be reserved yet. The fee is added to the summary.
  • Validation: first and last name, email, an optional 10-digit US phone number and a deliverable ZIP code.
  • Success: "You’re in Batch 6, Sam." with a reference BB-<batch>-<4 characters> from a hash of the email and the date (reservationRef(); sam@example.com on October 31, 2026 gives BB-06-26CD), the ship month, the $150 refundable deposit and what happens next for the delivery zone. The reservation is saved under sprocket:reservation.
  • Return visit: the saved reservation is shown instead of the form, with "Change my build" and "Cancel reservation" (a second "Yes, cancel it" step). Cancelling removes it and shows the form again with "Reservation BB-06-26CD cancelled. Your $150 deposit will be refunded in full."

No payment is taken: see Forms to connect a real backend or payment provider.

Cost calculator#

/specs, in src/components/CostCalculator.tsx and costFor(), settings in COST: car = miles a week × 52 × cost a mile + parking × 12; Portage = miles × 52 × 15 Wh ÷ 1000 × $0.16 + $240 upkeep, plus the $3,895 base price over 6 years. Payback = base price ÷ ((car − Portage running costs) ÷ 12), shown as months, "Over 10 years" when longer or when riding costs more. Defaults (40 mi, $0.70, $0 parking): car $1,456, Portage $894 ($245 running), you keep $562 a year, payback 39 months.

Build log and articles#

/journal (JournalList.tsx) renders POSTS with a category filter (chips with aria-pressed; the choice is kept in ?cat=). /article?a=<slug> (ArticleView.tsx) renders one post; a missing slug shows the latest post, an unknown one shows the latest post with a notice. The tab title and meta description follow the post once the page has loaded.

Demo dates#

Demo sites stay online for years, so nothing dated goes stale:

  • Batches are worked out from today (see Production batches).
  • Journal dates: with DEMO.KEEP_DEMO_CURRENT on, every post date moves forward by the whole weeks elapsed since DEMO.DEMO_ANCHOR (2026-10-31) in postsOn(), so weekdays stay right and the newest post stays recent. On the anchor date nothing moves. For a real site, set KEEP_DEMO_CURRENT: false and write real dates.
  • Copyright year: <YearSpan /> in the footer is filled in from today.
  • Previewing: add ?now=YYYY-MM-DDTHH:MM to any page (Central time) to see it on another date, for example /?now=2027-03-02T09:00. The configurator keeps the parameter in its URL.

Team and roles#

/about renders PEOPLE as cards with portraits and RolesList.tsx renders ROLES filtered by TEAMS (chips; the filter is kept in ?team=). Each role's "Apply" links to /support?topic=careers#contact, which preselects the topic.

Support page#

The FAQ search ([data-faq-search]) hides questions that don't contain the text and shows "3 questions match" or a "No questions match" message. The ZIP checker uses the same zoneFor() as the reservation. The contact form reads ?topic= (one of the select's values) to preselect a topic.

Header, menu, reveal and back to top#

The header turns sticky with a shadow after scrolling. Below 1024px a menu button opens the mobile menu (aria-expanded, Escape closes it and returns focus). Elements with .reveal fade in when scrolled into view (RevealController.tsx), off under reduced motion. BackToTop.tsx appears after a screen of scrolling.

Forms#

The demo forms validate in the browser and show a success state, but send nothing until you connect them.

Form Fields Validation
Reservation ([data-reserve-form], Reserve.tsx) first, last, email, phone (optional), zip, updates names required, email format, 10-digit phone if given, deliverable ZIP
Contact ([data-contact-form], ContactForm.tsx) name, email, topic, ref (optional), message name, email format, reference like BB-06-K7QD if given, message 10+ characters
ZIP checker ([data-zip-check], ZoneChecker.tsx) zip 5 digits

To connect the contact form to a service such as Formspree, find the comment "To receive messages" in submit in ContactForm.tsx, make the handler async and post the fields before showing the success state:

tsx
const res = await fetch("https://formspree.io/f/your-id", {
  method: "POST",
  headers: { Accept: "application/json" },
  body: new FormData(e.currentTarget),
});
if (!res.ok) return; // show an error and keep the form

(Read e.currentTarget before the first await.) With server features enabled (see Building and deploying), you can post to a Route Handler instead, for example src/app/api/contact/route.ts with export async function POST(request: Request) that reads await request.formData() and sends an email.

For real reservations, replace the code after the comment "To take real reservations" in Reserve.tsx with a call to your backend or a payment provider's checkout for the $150 deposit, and show the success state only when it confirms. Keep the line about the deposit being refundable accurate to your terms.

Saved data (localStorage)#

Key Shape Validation on read
sprocket:build { "color": "harbor", "battery": "dual", "acc": ["rack", "kidseat"] } color must be a key of BUILD.colors, then normalizeBuild()
sprocket:reservation { ref, first, last, email, phone, zip, updates, build, total, batch, shipNo, created } reference pattern, strings, integer batch and month number, valid created date, build normalized

All access goes through src/lib/store.ts and safeStorage() in src/lib/safe-storage.ts: localStorage when available, otherwise an in-memory stand-in, so saving and reserving still work for the page view in a sandboxed iframe or with storage blocked. useSavedBuild() and useSavedReservation() repaint every component that reads them when a value changes, also in another tab. To reset, use "Forget it" and "Cancel reservation", or clear the site's storage in the browser's developer tools.

SEO and sharing#

Every page sets its title, description and Open Graph tags with pageMetadata() (src/lib/metadata.ts), which keeps the social image public/img/og-image.png; the layout sets theme-color. Set NEXT_PUBLIC_SITE_URL to your domain when you build so metadataBase gives the image an absolute URL:

sh
NEXT_PUBLIC_SITE_URL=https://yourdomain.example pnpm build

/article updates the tab title and description for the post shown. Add a sitemap.xml listing the pages and a robots.txt when you deploy (or src/app/sitemap.ts and src/app/robots.ts). If you add Product structured data, use your real prices and availability.

Accessibility#

  • Skip link, landmarks (header, nav, main, footer), one <h1> per page and a logical heading order.
  • Visible focus styles; the color swatches are a radio group with arrow-key support; filters are buttons with aria-pressed; options are real radios and checkboxes.
  • Live regions announce the hero color, accessory notes, build status, range result, ZIP results and counts.
  • Form errors are linked with aria-describedby and fields get aria-invalid; the first invalid field is focused; success states receive focus.
  • The bike drawing has an aria-label describing the current build; decorative SVG is aria-hidden.
  • Reveals, transitions and the back-to-top scroll respect prefers-reduced-motion. Text colors meet WCAG AA.

When editing, keep one <h1> per page, label every new input and keep aria-controls/aria-describedby ids in step with the markup.

Building and deploying#

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

  • 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 from a sub-folder, build with DEMO_BASE_PATH=/bikes pnpm build; next.config.ts passes it to basePath and NEXT_PUBLIC_BASE_PATH, which asset() uses. pnpm build:demo does this for the freeframe.dev demo (demo/, which you can delete).

Tailwind scans the source files for class names; keep each class name whole (not built from pieces such as `bg-${color}`), or it won't be generated.

To use server features (Route Handlers, Server Actions), remove output: "export" and images.unoptimized from next.config.ts and deploy to a Node-capable host such as Vercel.

TypeScript setup#

package.json installs @typescript/native (TypeScript 7, which provides tsc) and typescript as an alias of @typescript/typescript6, because typescript-eslint and parts of the Next.js toolchain still need the TypeScript 6 API. When typescript-eslint supports TypeScript 7, replace both with typescript@7. eslint.config.mjs pins settings.react.version, because eslint-plugin-react can't detect it under ESLint 10, and turns off @next/next/no-img-element, because a static export serves plain <img> tags.

Troubleshooting#

  • The batch, spots left or dates look wrong. They are worked out in Central time from today; check for a leftover ?now= in the URL, or use ?now=2027-01-15T09:00 to check another date, and edit BATCHES in src/data/site.ts.
  • The page briefly shows Batch 6 or the standard Portage. That is the text written for the static build; it is replaced as soon as the page loads. If it stays, check the browser console for a JavaScript error.
  • A shared configurator link opens the default bike. Check the parameter values: they must be keys from BUILD (?color=birch, not ?color=Birch White).
  • The saved build or reservation disappears on reload. The browser is blocking storage (private mode, a sandboxed iframe); it then only lasts for the page view.
  • An accessory doesn't appear on the bike. It needs a <g {...part("<key>")}> group in BikeArt.tsx.
  • Copy link does nothing. Clipboard access needs https:// or localhost; on other origins the page shows the link in a field to copy instead.
  • Images or the social image are missing when the site lives in a sub-folder. Build with DEMO_BASE_PATH set to the folder, and pass new image paths through asset().

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. Fonts: Kanit and Encode Sans are licensed under the SIL Open Font License. Bonfield Bikes, the Portage, its people, press quotes, backers, phone number and email address are fictional placeholders.

Search documentation

Search the guides and every template's documentation