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 product launch and preorder website for a hardware startup, built as a WordPress block theme. The fictional brand is Bonfield Bikes, an e-bike maker in Duluth, Minnesota, taking reservations for the Portage, a compact cargo e-bike built for hills and winter. The theme has a home page with a bike drawing you can recolor and the open production batch; a configurator with five frame colors, two batteries and six accessories, a live price, shareable links and a saved build; a range estimator; a reservation page with delivery zones by ZIP code, a refundable deposit and a saved reservation; a specs page with a comparison table and a car vs e-bike cost calculator; a build log with a category filter and an article page; an about page with team portraits, backers and open roles; a support page with FAQ search, warranty, a ZIP checker and a contact form; and a 404 page. It is version 1.0.0 (see template.json and style.css), works with WordPress 7.1 and PHP 7.4 or newer, and is the WordPress version of the HTML template of the same name. See it live at freeframe.dev/templates/sprocket.

Headings, copy, cards, lists, buttons and FAQs are core WordPress blocks arranged in block patterns, so you edit them in the block editor without touching code. The parts that are worked out in the browser (the hero bike and swatches, the configurator, the range estimator, the reservation flow, the cost calculator, the build log lists, the article body, the team and roles, the FAQ search, the ZIP checker and the contact form) work as in the HTML version: they are Custom HTML blocks driven by assets/js/main.js, which reads the company's data from assets/js/data.js. Colors and fonts are design tokens in theme.json: change them in Appearance > Editor > Styles and the whole site follows. Every date is worked out in America/Chicago (Central time), whatever the visitor's own time zone is.

Quick start#

Requirements: WordPress 7.1 or newer and PHP 7.4 or newer. No plugins are needed.

  1. Download sprocket.zip from freeframe.dev/templates/sprocket (the WordPress tab).
  2. In your WordPress admin go to Appearance > Themes > Add New Theme > Upload Theme, choose the zip and click Install Now, then Activate. The theme is listed as Freeframe Sprocket.
  3. Go to Appearance > Sprocket setup and click Create the demo pages. This creates the Home, Build your Portage, Range estimator, Reserve a Portage, Portage specs and cost calculator, Build log, the article page, About us and Support pages from the theme's page patterns, sets Home as the front page (Settings > Reading) and creates the header menu. Pages that already exist with the same slug are left alone, so it is safe to click it again.
  4. Visit your site. Edit any page in Pages, and the header, footer and templates in Appearance > Editor.

You can also try the theme without installing anything: the Try it in WordPress button on freeframe.dev opens it in WordPress Playground (WordPress running in your browser) with the demo pages already set up.

To work on the theme's files locally you need Node.js 24 and pnpm; WordPress itself runs in WordPress Playground, so there is no PHP, web server or database to install:

sh
pnpm install
pnpm dev     # builds the CSS, then starts WordPress with the theme at http://127.0.0.1:9400

pnpm dev signs you in as admin (password password) and creates the demo pages. The site is rebuilt from scratch every time you start it; your edits to theme files are live, edits made in the WordPress admin are not kept.

Pages#

Each page is a page pattern (in Patterns > Sprocket pages) made of section patterns. Every section is also offered on its own in the block inserter under Patterns > Sprocket.

Page Slug Page pattern Sections (pattern files in patterns/)
Home / page-home.php home-hero (bike drawing and color swatches), batch-panel (the open batch), why-cargo, built-for-here, press, home-faq, home-cta
Build your Portage /bike/ page-bike.php configurator (drawing, colors, batteries, accessories, price, save and share), included
Range estimator /range/ page-range.php range-estimator
Reserve a Portage /reserve/ page-reserve.php reserve (form, success state, saved reservation, build summary)
Portage specs and cost calculator /specs/ page-specs.php specs-header, specs-list (spec tables and footprint drawing), specs-compare, cost-calculator
Build log /journal/ page-journal.php journal (category filter, featured post and grid built from POSTS)
Article /article/ page-article.php article (filled from POSTS by ?a=), article-more
About us /about/ page-about.php about-hero, timeline, team (built from PEOPLE), backers, careers (#careers, roles built from ROLES)
Support /support/ page-support.php support-header, support-faq (#faq), warranty (#warranty), delivery (#delivery), contact (#contact)
404 any missing URL templates/404.html 404

The article page works as in the HTML version: one page, filled in the browser from assets/js/data.js. /article/?a=<slug> shows that post; with no parameter it shows the latest post, and an unknown slug shows the latest post with a notice. Every link the theme writes to a post uses this address. WordPress posts use templates/single.html; the blog, archives and search results use templates/index.html, templates/archive.html and templates/search.html.

Project structure#

freeframe-sprocket/
  style.css                 theme header (name, version, requirements, license); no styles
  theme.json                design tokens: palette, fonts, font sizes, spacing, layout widths, button
  functions.php             loads the files in inc/
  inc/setup.php             stylesheet and scripts (with the script's strings and page URLs),
                            editor styles, the `js` class script, theme color and favicon,
                            title separator, pattern categories
  inc/icons.php             registers assets/icons/*.svg for the Icon block (none ship by default)
  inc/icon-labels.php       the icons' names shown in the editor
  inc/block-styles.php      button block styles
  inc/site-setup.php        Appearance > Sprocket setup: creates the demo pages and the menu
  templates/                page.html (pages), page-with-title.html, single.html, index.html,
                            archive.html, search.html, 404.html
  parts/                    header.html and footer.html (they show the header and footer patterns)
  patterns/                 every section and page (PHP files with block markup)
  assets/css/theme.css      the compiled stylesheet (do not edit: built from src/)
  assets/css/editor.css     the same for the block editor (generated by src/editor-css.mjs)
  assets/js/data.js         the company's data: COMPANY, BUILD, BATCHES, ZONES, RANGE, COST, DEMO,
                            CATEGORIES, POSTS, PEOPLE, TEAMS, ROLES (window.SPROCKET)
  assets/js/main.js         batches, hero, configurator, range, reserve, cost calculator, build log,
                            team and roles, support, header, reveal and back to top; also the
                            journal covers and team portraits
  assets/img/               logo.svg and logo-dark.svg (header and footer), favicon, social image
  assets/fonts/             Kanit and Encode Sans, with their licenses
  src/theme.css             the Tailwind CSS source of assets/css/theme.css
  src/icons.css             icons drawn before or after text with CSS (generated)
  src/editor-css.mjs        writes assets/css/editor.css after each build
  languages/                freeframe-sprocket.pot, the template for translations
  blueprint.json            WordPress Playground setup used by `pnpm dev`
  screenshot.png            the picture shown in Appearance > Themes
  package.json              the CSS build and the local WordPress (Node.js tooling)
  readme.txt, README.md, DOCS.md, CHANGELOG.md, LICENSE.txt

Customizing#

Brand and colors#

The colors are in theme.json (settings.color.palette) and in Appearance > Editor > Styles > Colors > Palette:

Color Default Used for
Snow, Snow 2 #f3f2ed, #e8e6de the page background, sunken bands and table stripes
Card #ffffff cards and forms
Line, Line strong #d9d6cb, #b5b1a3 hairlines and field borders
Ink #14171a text, borders and dark bands (iron)
Ink 2, Ink 3 #1e2327, #30373c raised panels and borders on ink
Muted #575c5f secondary text on light backgrounds
Frost #c9d3d6 secondary text on ink
Ore (Taconite red) #c63d1f buttons, links, the logo, the slash in headlines
Ore deep, Ore soft #a8301a, #f7dbd2 hover and tints
Ember #ff8a66 the red on dark backgrounds
Lake (Harbor teal), Lake deep, Lake soft, Ice #0f6672, #0b4e57, #d4e8ea, #9fd9e3 teal accents; Ice is teal on ink
OK, OK soft, Bad #2d6a3e, #dcefe0, #a8301a success and error text
White, Black for your own blocks

The theme's stylesheet reads these colors from the palette (bg-ore, text-muted...), so changing Ore in Styles recolors every button and link at once. Keep Muted dark enough for text on Snow (4.5:1), and use Ember, not Ore, for text on Ink. The bike drawing, maps, journal covers, portraits and the 404 drawing carry their own colors in their markup; the logo marks are assets/img/logo.svg (header), assets/img/logo-dark.svg (footer) and assets/img/favicon.svg. The bike's frame color is not a token: it comes from the chosen color in BUILD.colors (see Configurator).

Components worth knowing in src/theme.css: headline (heavy italic uppercase Kanit), display and section-title (headline sizes), slash (the red words in headlines), eyebrow (labels with a slanted square), lede, on-dark (light text and ember accents on an ink or red band), card, card-soft, lift (the hard offset shadow), tag, chip, stat, tread, tread-ore and tread-light (the diagonal tire-tread stripes), link-arrow and text-link. Add them to a block in Block settings > Advanced > Additional CSS class(es).

Typography#

Two fonts are bundled in assets/fonts/ and declared in theme.json (settings.typography.fontFamilies), so no request goes to Google: Kanit (500, 600, 700 and 800, with 700 and 800 italic; preset display) for headlines, buttons, labels and numbers, and Encode Sans (400 to 700; preset sans) for everything else. To use other fonts, install them in **Styles

Typography > Fonts** (the Font Library) and select them, or replace the fontFace entries in theme.json. The utility classes read the presets (--wp--preset--font-family--display, --sans), so keep those slugs. The drawings with lettering (the hill on the home page, the specs footprint, the delivery map, the about map, the journal covers, the 404 drawing) name Kanit directly (font-family="Kanit, sans-serif").

Font sizes are the presets in theme.json (xs 0.75rem to 9xl 8rem, the Tailwind scale). Prices and counts use tabular, lining numerals through the num class.

Content#

Edit pages in Pages: click any text to change it, select a button to change its link. Sections are groups; open List View (the stacked lines icon at the top left) to select a section, move it, duplicate it or delete it. To add a section from another page, use the block inserter (+) > Patterns > Sprocket.

What the script fills in or drives is in Custom HTML blocks: edit those in the block's HTML view, and keep their ids and data- attributes.

Pattern Custom HTML What it is
home-hero the bike drawing, its backdrop and the color swatches see Home hero
batch-panel the spots-left meter see Production batches
built-for-here the feature drawings small SVG drawings
configurator the whole configurator see Configurator
range-estimator the form and the result panel see Range estimator
reserve the form, success state, saved reservation and the build summary see Reservation
specs-list, specs-compare the spec tables, the footprint drawing and the comparison table edit the rows in HTML view
cost-calculator the sliders and the result panel see Cost calculator
journal the category chips, the featured post and the grid see Build log and articles
article, article-more the notice, the dek, the cover, the body, the author and "More from the shop" see Build log and articles
about-hero the hill map an SVG map of Duluth
team, careers the team cards, the role chips and the roles list see Team and roles
support-faq the search box see Support page
delivery the ZIP checker, its result, the zone list and the zone map see Support page
contact the contact form and its success panel see Forms
404 the wheel drawing an SVG drawing
header the menu button driven by the script

Some blocks carry classes (Block settings > Advanced > Additional CSS class(es)) that the script reads. Blocks can't store data- attributes, so the script turns classes starting with ffd- into them when the page loads: ffd-batch-line on the header's first line becomes data-batch-line, ffd-faq-list on the support FAQ group becomes data-faq-list, and ff-hidden becomes the hidden attribute. Spans inside text keep their attributes as they are (<span data-batch-name>Batch 6</span> in a heading is filled with the open batch). The first section of each page has a class ff-page-<name> (ff-page-bike) that tells the script which page it is on. Keep those classes when you restyle a block.

  • reveal fades a block in as it scrolls into view.
  • link-card on a group makes the whole group clickable: the first link inside it stretches over the group (the logo in the header and footer).

Everything data-driven is in assets/js/data.js, inside window.SPROCKET, the same file as in the HTML version:

Constant What it holds
COMPANY name, phone, email, address, timeZone (America/Chicago), reference prefixes (BB for reservations, BS for support requests)
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

Change data.js in a child theme (see Child themes) so an update doesn't overwrite it. Prices also appear in block text (the hero's "From $3,895", the specs tables, the FAQ answers); when you change BUILD, search the pages for the old amounts too.

Adding an accessory

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

js
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>…</g> group inside the bike SVG in the Custom HTML blocks of the home-hero, configurator and reserve patterns (the three copies are the same drawing).

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 main.js and a body made of ['p', text], ['h2', text], ['ul', [items]] and ['quote', text, who] blocks. It appears in the build log and at /article/?a=<slug>.

Images and artwork#

All art is SVG drawn for this template; there are no photos.

  • The Portage is inline SVG in the Custom HTML blocks of home-hero, configurator and reserve (viewBox 800 × 500). The frame strokes have the class frame and take their color from the --frame custom property on the <svg>; each optional part is a group with data-part (rack, kidseat, panniers, basket, studs, pogies, battery-dual). To use photos instead, replace the SVG with one image per color and swap its src in paintBike() in main.js; keep a 16:10 frame.
  • Journal covers are drawn by the ART functions in main.js (640 × 400); cover(p) picks ART[p.art].
  • Team portraits are drawn by portrait(p) from the skin, hair, style (short, long, bun, bald, curly), shirt, glasses and beard values in PEOPLE (square).
  • The hill map (about), the delivery zone map (support), the footprint chart (specs), the feature drawings (home) and the 404 drawing are inline SVG in Custom HTML blocks, so their lettering uses Kanit; replace any of them with an Image block if you prefer.
  • The social image is assets/img/og-image.png (1200 × 630, source og-image.svg); the favicon assets/img/favicon.svg is used until you set a Site Icon in Settings > General.

The header and footer are template parts (parts/header.html, parts/footer.html) showing the header and footer patterns. Edit them in Appearance > Editor > Patterns > Header / Footer. The header pattern starts with the dark batch bar: its first line is replaced by the open batch when the page loads (the link written there is what shows without JavaScript). Delete that group to remove the bar.

The header has two Navigation blocks showing the same menu: the desktop one and the one in the mobile menu. Edit the menu once in Appearance > Editor > Navigation (the setup creates "Main menu": Build yours, Range, Specs, Journal, About, Support). WordPress marks the current page's link with aria-current="page"; on the article page the script marks Journal, as the HTML version does (the page URLs are in freeframe_sprocket_page_urls() in inc/setup.php). A Navigation block that doesn't point at a menu shows the most recently created one; to pin a menu, select the Navigation block and choose it in the block settings, for both blocks.

The footer's The Portage and Company menus are List blocks: click a link to change it, press Enter to add one. The copyright year is written by PHP. The phone number and email appear in the batch bar, the mobile menu, the footer and the support page; search for them when you change them (and in COMPANY in data.js).

Features#

Production batches#

batchFor(dateKey) in main.js works out the batch open on a date from BATCHES in data.js:

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

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. In block text these are spans: in the editor, open the block's Edit as HTML to see them. For a real launch with fixed dates, replace batchFor() with your own table of batches.

Home hero#

The swatches under the hero bike ([data-hero-swatches], an ARIA radio group: arrow keys move and select) recolor it and update the "Reserve yours" and "Build yours" links (data-hero-reserve, data-hero-build) so the chosen color carries over. The hero starts from the saved build when there is one.

Configurator#

/bike/: the options are rendered from BUILD in data.js into [data-colors] (an ARIA radio group of swatches), [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 --frame, shows the matching data-part groups (the dual battery shows battery-dual) and updates the SVG's aria-label.
  • URL: every change updates the address with history.replaceState: /bike/?color=harbor&battery=dual&acc=rack,kidseat. "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.

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.

Range estimator#

/range/, formula in RANGE in data.js:

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 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 18 mi dual (113% / 79%)
School run 28 mi 40 mi standard
Summer grocery run 56 mi 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 ZONES by its first three digits: 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 can't be reserved yet. The fee is added to the summary (90210 on a $5,005 build gives $5,254).
  • 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.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/, 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 in months, or "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=): "2 posts in Testing". /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 follows 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_CURRENT on, every post date moves forward by the whole weeks elapsed since DEMO.DEMO_ANCHOR (2026-10-31), so weekdays stay right and the newest post stays recent. For a real site, set KEEP_DEMO_CURRENT: false and write real dates.
  • Copyright year: written by PHP in the footer.
  • 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#

The about page renders PEOPLE as cards with portraits and ROLES as a list filtered by TEAMS (chips; the filter is kept in ?team=, for example "2 open roles in Owner care"). Each role's "Apply" links to /support/?topic=careers#contact, which preselects the topic.

Support page#

The FAQ is made of Details blocks inside a group with the class ffd-faq-list; add a question by duplicating one. The search box hides questions that don't contain the text and shows "1 question matches" (for "winter") or a "No questions match" message. The ZIP checker uses the same zoneFor() as the reservation ("55807 is in our Duluth–Superior hand delivery zone: delivery free."). The contact form reads ?topic= (one of the select's values) to preselect a topic.

Header, mobile menu, reveal and back to top#

The header is sticky and gains a shadow after scrolling. Below 1024 px the menu button (#menu-toggle, aria-expanded, aria-controls="mobile-menu") opens the mobile menu; Escape closes it and returns focus to the button, and it also closes on a link click or when the window grows. Blocks with reveal fade in as they scroll into view (off with reduced motion). A back-to-top button appears after about one screen of scrolling; to remove it, delete the "Back to top" part at the end of assets/js/main.js and the .back-to-top styles in src/theme.css, then run pnpm build.

Block styles#

In the block settings' Styles panel:

Block Styles
Button Fill (default, the Taconite red button), Iron, Outline, Outline (on dark)

The class is-small on a Button block makes it the smaller header size. The FAQ answers are Details blocks with the class faq-item, which draws the plus/minus sign.

Forms#

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

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

Errors show inline with aria-invalid, and the first invalid field is focused. The contact form's success panel says "Thanks, Sam." with a request number BS- + four digits.

To connect the contact form to a service such as Formspree, find the comment "To receive messages" in assets/js/main.js (in a child theme) and post the fields before showing the success state:

js
const res = await fetch('https://formspree.io/f/your-id', {
  method: 'POST',
  headers: { Accept: 'application/json' },
  body: new FormData(contactForm)
});
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. You can also replace a form's Custom HTML block with a form plugin's block; add the classes label, field and btn-ore to its labels, fields and button so it matches.

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

These are the HTML version's keys. All access goes through safeStorage in main.js: 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 remove the keys in your browser's developer tools (Application > Local Storage).

SEO and sharing#

Page titles come from WordPress: the page title, a vertical bar and the site name (for example "Range estimator | Bonfield Bikes"); the home page shows the site name and tagline (Settings > General). The article page also sets document.title for the post shown. The theme doesn't output meta descriptions or Open Graph tags; use an SEO plugin such as Yoast SEO, Rank Math or The SEO Framework, and give it assets/img/og-image.png (or your own 1200 × 630 image) as the default social image. WordPress already generates a sitemap at /wp-sitemap.xml. If search traffic to individual build log posts matters, publish them as WordPress posts instead of the ?a= page. If you add Product structured data, use your real prices and availability.

Accessibility#

Built in: a skip link to the main content (added by WordPress, styled by the theme), landmarks (header, labelled navigation, main, footer), one h1 per page, labelled sections, visible focus outlines (red on light, ember on dark), the menu button with aria-expanded, the color swatches as a radio group with arrow keys, filter chips as buttons with aria-pressed, options as real radios and checkboxes, live regions for the hero color, accessory notes, build status, range result, ZIP results and counts, form errors linked with aria-describedby and aria-invalid, success states that receive focus, an aria-label on the bike drawing that describes the current build, and reduced-motion support for every animation. The demo passes axe (WCAG 2.1 A and AA).

When you edit: keep one Heading 1 per page, give images alt text (or mark them decorative), keep link text meaningful, label any field you add, and keep aria-controls and aria-describedby ids in step.

Building and deploying#

You only need this if you change the theme's files. The stylesheet assets/css/theme.css is compiled with Tailwind CSS from src/theme.css and the classes used in patterns/, parts/, templates/, inc/ and assets/js/:

sh
pnpm install
pnpm build   # compile assets/css/theme.css and assets/css/editor.css
pnpm watch   # recompile assets/css/theme.css on every change (run pnpm build before you commit)

Run pnpm build after adding a utility class to a pattern, template or the script's markup, otherwise the class has no style; keep each class name whole in the script (not built from pieces). Classes you type in the editor's Additional CSS class(es) field are only styled if the stylesheet already contains them.

To deploy, zip the theme folder (named freeframe-sprocket, without node_modules/) and upload it in Appearance > Themes, or copy the folder to wp-content/themes/ on your server.

Translations#

The theme is translation-ready with the text domain freeframe-sprocket. All text in patterns, templates and PHP goes through WordPress's translation functions; the script's own strings (month and day names, batch lines, price labels, the range working, form errors and messages) are translated in PHP (freeframe_sprocket_script_strings() in inc/setup.php). The company's content in assets/js/data.js (colors, accessories, zones, posts, people, roles) is edited there directly. Create a translation with Loco Translate, or with WP-CLI and Poedit:

sh
wp i18n make-pot wp-content/themes/freeframe-sprocket wp-content/themes/freeframe-sprocket/languages/freeframe-sprocket.pot

Save freeframe-sprocket-es_ES.mo (for example) in wp-content/languages/themes/ or in the theme's languages/ folder. Page content created by Sprocket setup is copied into your pages in the site's language at that moment; translate it in the editor like any other content. Amounts are formatted in US dollars and dates in US style by the script (money(), pubDate(), dayMonth()).

Child themes#

Use a child theme for changes you want to keep when Sprocket is updated (your prices, batches, zones and posts in particular). Create wp-content/themes/sprocket-child/style.css:

css
/*
Theme Name: Sprocket Child
Template: freeframe-sprocket
*/

Templates, parts, patterns and theme.json in the child theme override the parent's files of the same name. To use your own data.js, dequeue the parent's data script (freeframe-sprocket-data) and enqueue yours under the same handle in the child's functions.php:

add_action( 'wp_enqueue_scripts', function () {
	wp_deregister_script( 'freeframe-sprocket-data' );
	wp_register_script( 'freeframe-sprocket-data', get_stylesheet_directory_uri() . '/data.js', array(), '1.0', array( 'in_footer' => true, 'strategy' => 'defer' ) );
}, 15 );

Do the same for assets/js/main.js (handle freeframe-sprocket), keeping the inline window.freeframeSprocket strings and URLs (or copy them). The parent's stylesheet only contains the classes the parent uses: if your child theme uses new utility classes, compile a stylesheet for the child (copy package.json and src/theme.css, point the @source lines at the child's folders) and enqueue it.

Troubleshooting#

  • The pages look unstyled or new classes do nothing. The stylesheet only contains classes it found when it was built. Run pnpm build after editing patterns, then reload.
  • "This block contains unexpected or invalid content". A pattern or page was edited by hand with markup that doesn't match the block. Click Attempt recovery, or re-insert the section from Patterns > Sprocket.
  • 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; it freezes the date. Edit BATCHES in data.js.
  • Configurator, reserve or article links go to the wrong page. The script links to /bike/, /reserve/, /article/, /journal/ and /support/. If you renamed those pages, change their URLs in freeframe_sprocket_page_urls() in inc/setup.php (in a child theme, filter the inline script).
  • A shared configurator link opens the default bike. The parameter values 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 preview, strict privacy settings); it then lasts for the page view only.
  • An accessory doesn't appear on the bike. It needs a <g data-part="<key>"> group in each of the three bike drawings.
  • Sprocket setup says the pages exist, but they are empty. It never overwrites existing pages. Delete (and empty the trash of) the pages with those slugs, or open each page and insert its page pattern from Patterns > Sprocket pages.

License#

Split license: the PHP code and block markup are licensed under the GNU GPL, version 2 or later, and the CSS, JavaScript, images and design under the Freeframe Commercial License (unlimited sites for you and your clients; no resale or redistribution as a theme). 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 1.1 (OFL.txt in each folder of assets/fonts/). 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