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. This is the Astro 7 + TypeScript + Tailwind CSS v4 version, version 1.0.0. It has no UI framework: pages are rendered to static HTML at build time and the interactive parts are plain TypeScript. 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 or newer and pnpm (npm works too).
pnpm install
pnpm dev # http://localhost:4321With npm: npm install, then npm run dev. Add ?now=2027-03-02T09:00 to any page's URL to preview it at a
Central date and time (see Demo dates).
| 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/sprocket into demo/ (only used for the demo on freeframe.dev) |
Pages#
Every file in src/pages/ becomes one page, built as <name>.html and linked as /<name>.
| URL | File | Notable sections and features |
|---|---|---|
/ |
src/pages/index.astro |
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 |
src/pages/bike.astro |
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 |
src/pages/range.astro |
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 |
src/pages/reserve.astro |
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 |
src/pages/specs.astro |
Key figures, full specifications, footprint drawing, comparison table, car vs Portage cost calculator |
/journal |
src/pages/journal.astro |
Build log: category filter (?cat=), the latest post featured, the rest in a grid, each with a drawn cover |
/article?a=<slug> |
src/pages/article.astro |
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 |
src/pages/about.astro |
Story with a hill map, timeline, seven illustrated team portraits, backers, open roles with a team filter (?team=, #careers) |
/support |
src/pages/support.astro |
FAQ with search, warranty, delivery zones with a ZIP checker, contact form with success state (?topic=) |
| 404 | src/pages/404.astro |
A wheel rolling down the hill and links to the main pages (most hosts show 404.html automatically) |
Project structure#
sprocket/
├─ src/
│ ├─ pages/ one .astro file per page (see above)
│ ├─ layouts/
│ │ └─ Layout.astro <head> (title, meta, Open Graph, fonts), skip link, header, footer, scripts
│ ├─ components/
│ │ ├─ SiteHeader.astro batch bar, logo, main menu, Reserve button, mobile menu
│ │ ├─ SiteFooter.astro footer columns, shop address, hours and copyright
│ │ ├─ Logo.astro the sprocket mark used in the header and footer
│ │ └─ Bike.astro the Portage drawing, used on the home, configurator and reserve pages
│ ├─ data/
│ │ ├─ sprocket.ts all content and settings: COMPANY, BUILD, BATCHES, ZONES, RANGE, COST, DEMO, POSTS, PEOPLE, ROLES
│ │ └─ nav.ts header menu items and the footer link columns
│ ├─ lib/
│ │ └─ site.ts site name, theme color and the url() helper for links
│ ├─ scripts/
│ │ ├─ main.ts all behavior (reads src/data/sprocket.ts), plus the article covers and portraits
│ │ └─ back-to-top.ts floating "Back to top" button
│ └─ styles/
│ └─ global.css Tailwind theme (@theme tokens), a few utilities and component classes
├─ public/img/
│ ├─ favicon.svg the sprocket mark
│ └─ og-image.svg, og-image.png social image source and the PNG used in og:image (1200×630)
├─ astro.config.mjs site URL, base path, output format, Tailwind plugin
├─ eslint.config.mjs, tsconfig.json
├─ package.json, pnpm-workspace.yaml, pnpm-lock.yaml
└─ README.md, DOCS.md, CHANGELOG.md, template.jsonPages are rendered to HTML at build time. The two files in src/scripts/ are the only JavaScript sent to the
browser, plus one inline line in the layout that adds the js class to <html> before the page paints.
Layout.astro imports both scripts and Astro bundles them, with the data they import from
src/data/sprocket.ts, into one small module. Each page passes page to the layout, which becomes
<body data-page="…">.
Customizing#
Brand and colors#
The design tokens are in the @theme block of src/styles/global.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 (pnpm dev picks it up straight away), and update the
few hex values written into SVG art (the logo mark in src/components/Logo.astro, public/img/favicon.svg, the
hero backdrop in src/pages/index.astro):
@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#
Headlines use Kanit (heavy italic uppercase, via the headline utility) and body text uses Encode Sans.
Both are loaded from Google Fonts with a <link> in src/layouts/Layout.astro:
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Encode+Sans:wght@400..700&family=Kanit:ital,wght@0,500;0,600;0,700;0,800;1,700;1,800&display=swap" />To swap them, change that link and the --font-sans and --font-display tokens in src/styles/global.css.
public/img/og-image.svg names the fonts too.
Content#
Page copy lives in the .astro files in src/pages/. Everything that is listed, priced or calculated lives in
src/data/sprocket.ts, with TypeScript types for each structure:
| 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):
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 data-part="lock" {...{ hidden: true }}>…</g> group inside the SVG in
src/components/Bike.astro (the one drawing is used on all three pages).
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/scripts/main.ts and a body made of ["p", text], ["h2", text], ["ul", [items]] and
["quote", text, who] blocks.
Images and artwork#
All artwork is SVG drawn for this template, no photos:
- The Portage is inline SVG in
src/components/Bike.astro(viewBox 800×500). The frame strokes have the classframeand take their color from the--framecustom property on the<svg>; each optional part is a group withdata-part(rack,kidseat,panniers,basket,studs,pogies,battery-dual). To use photos instead, replace the SVG with one image per color and swap itssrcinpaintBike()insrc/scripts/main.ts; keep a 16:10 frame. - Article covers are drawn by the
ARTfunctions insrc/scripts/main.ts(640×400);cover(p)picksART[p.art]. Replace them with images of the same ratio if you like. - Team portraits are drawn by
portrait(p)from theskin,hair,style(short,long,bun,bald,curly),shirt,glassesandbeardvalues inPEOPLE(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 toog-image.png(1200×630). The favicon ispublic/img/favicon.svg.
Navigation, header and footer#
The header is src/components/SiteHeader.astro: the thin batch bar (data-batch-line, the open batch on every
page; delete that <div> to remove it), the logo, the main menu, the "Reserve" button and the mobile menu
(#mobile-menu, toggled by #menu-toggle). The menu items (Build yours, Range, Specs, Journal, About,
Support) are mainNav in src/data/nav.ts, used by both the desktop and the mobile menu. Each page passes
current to <Layout> and the matching link gets aria-current="page" (article.astro passes "journal";
reserve.astro passes "reserve", which marks the Reserve button).
To add a page, create src/pages/<name>.astro with <Layout title=… description=… page="<name>" current="<id>">
and a <main id="main" class="flex-1">, then add { id, label, href } to mainNav. The footer is
src/components/SiteFooter.astro; its two link columns are portageLinks and companyLinks in src/data/nav.ts.
Write internal links with url() from src/lib/site.ts (href={url("/bike")}), never a bare "/bike", so they
keep working when the site is served from a sub-folder. Links built in src/scripts/main.ts use its link()
helper for the same reason.
Features#
Production batches#
batchFor(dateKey) in src/scripts/main.ts works out the batch open on a date from BATCHES in
src/data/sprocket.ts:
- Batch 1 ships in
firstShip(March 2026) and a batch ships everyevery(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 aboutminLeft(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.
Any element with one of these attributes is filled in: data-batch-name ("Batch 6"), data-batch-ship
("January 2027"), data-batch-left, data-batch-size, data-batch-taken, data-batch-close ("Saturday,
October 31"), data-batch-next and the width of data-batch-meter. For a real launch with fixed dates, replace
batchFor() with your own table of batches.
Configurator#
/bike: the options are rendered from BUILD in src/data/sprocket.ts 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:
paintBike(svg, build)sets--frameand shows the matchingdata-partgroups (the dual battery showsbattery-dual), and updates the SVG'saria-label. - URL: every change updates the address with
history.replaceState:/bike?color=harbor&battery=dual&acc=rack,kidseat. "Reserve this build" links to/reservewith 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#
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, formula in RANGE in src/data/sprocket.ts:
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.08The 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:
- 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
ZONESby its first three digits:556–558and548are Duluth–Superior hand delivery (free), most of Minnesota, Wisconsin, Iowa, the Dakotas and the Upper Peninsula are Upper Midwest freight ($149), the rest of010–994is 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:sam@example.comon October 31, 2026 givesBB-06-26CD), the ship month, the $150 refundable deposit and what happens next for the delivery zone. The reservation is saved undersprocket: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, 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 renders POSTS with a category filter (chips with aria-pressed; the choice is kept in
?cat=). /article?a=<slug> renders one post; a missing slug shows the latest post, an unknown one shows
the latest post with a notice. The page title and meta description follow the post.
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_CURRENTon, every post date moves forward by the whole weeks elapsed sinceDEMO.DEMO_ANCHOR(2026-10-31), so weekdays stay right and the newest post stays recent. On the anchor date nothing moves. For a real site, setKEEP_DEMO_CURRENT: falseand write real dates. - Copyright year:
<span data-year>in the footer is filled in from today. - Previewing: add
?now=YYYY-MM-DDTHH:MMto 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 ROLES as a list 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); while it is open, body.menu-is-open stops the page from
scrolling. Elements with .reveal fade in when scrolled into view, off under reduced motion. The back-to-top
button is src/scripts/back-to-top.ts: it appears after a screen of scrolling; to remove it, delete the file, its
import in src/layouts/Layout.astro and the .back-to-top styles in src/styles/global.css.
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]) |
first, last, email, phone (optional), zip, updates | names required, email format, 10-digit phone if given, deliverable ZIP |
Contact ([data-contact-form]) |
name, email, topic, ref (optional), message | name, email format, reference like BB-06-K7QD if given, message 10+ characters |
ZIP checker ([data-zip-check]) |
zip | 5 digits |
To connect the contact form to a service such as Formspree, find the comment "To receive messages" in
src/scripts/main.ts and post the fields before showing the success state:
const res = await fetch("https://formspree.io/f/your-id", {
method: "POST",
headers: { Accept: "application/json" },
body: new FormData(form),
});
if (!res.ok) {
/* show an error and keep the form */
return;
}(Make the submit handler async.) For real reservations, replace the code after the comment "To take real
reservations" 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. The
site is static, so the backend lives elsewhere (a form service, a serverless function or your shop's API); if you
add an Astro server adapter you can also handle the post in an Astro endpoint (src/pages/api/reserve.ts).
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 safeStorage in src/scripts/main.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. To
reset, use "Forget it" and "Cancel reservation", or clear the site's storage in the browser's developer tools.
SEO and sharing#
Each page passes its title and description to <Layout>, which writes the <title>, meta description,
canonical link, Open Graph tags (og:title, og:description, og:url, og:image, og:site_name, og:locale
en_US), a Twitter card and theme-color. /article also updates document.title and the description for the
post shown. The 404 page passes noindex.
Canonical and Open Graph URLs are absolute, built from site in astro.config.mjs: set SITE_URL when you build
(SITE_URL=https://www.yourdomain.example pnpm build) or change the default there. After editing
public/img/og-image.svg, regenerate the PNG (1200×630) with any SVG-to-PNG tool, keeping the Kanit and Encode
Sans fonts available so the text renders.
Suggestions: add @astrojs/sitemap and a public/robots.txt; give each post its own static page if search
traffic to them matters (the ?a= pages share one page; Astro's getStaticPaths() can generate one per slug from
POSTS); 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-describedbyand fields getaria-invalid; the first invalid field is focused; success states receive focus. - The bike drawing has an
aria-labeldescribing the current build; decorative SVG isaria-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 # static site in dist/
pnpm preview # preview dist/ locallydist/ is plain HTML, CSS and JavaScript. Pages are written as bike.html rather than bike/index.html
(build.format: "file" in astro.config.mjs), and links point to /bike without a trailing slash. Netlify,
Cloudflare Pages and GitHub Pages serve /bike from bike.html automatically; Vercel does with cleanUrls.
- 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/bikeservesbike.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/biketobike.html, changebuild.formatto"directory"inastro.config.mjs.
To serve the site from a sub-folder, set DEMO_BASE_PATH when building, for example
DEMO_BASE_PATH=/portage pnpm build. It becomes Astro's base; every link written with url() and every link the
script builds includes it. Set SITE_URL to your domain for canonical and Open Graph URLs.
Tailwind CSS is compiled by @tailwindcss/vite during pnpm dev and pnpm build; there is no separate CSS step.
It scans the .astro and .ts files for class names, so keep each class name whole in main.ts (not built from
pieces).
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#
- 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, use?now=2027-01-15T09:00to check another date, and editBATCHESinsrc/data/sprocket.ts. - 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.
- New styles don't show up. Restart
pnpm devif a token change doesn't appear, and hard-refresh. A class assembled from pieces in TypeScript ("bg-" + color) isn't generated; write the full class name. - An accessory doesn't appear on the bike. It needs a
<g data-part="<key>">group insrc/components/Bike.astro. - Links break when the site is in a sub-folder. Build with
DEMO_BASE_PATH=/<folder>and write internal links withurl(); inmain.ts, uselink(). - Copy link does nothing. Clipboard access needs
https://orlocalhost; on other origins the page shows the link in a field to copy instead.
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 and served by Google Fonts. Bonfield Bikes, the
Portage, its people, press quotes, backers, phone number and email address are fictional placeholders.