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.
- Download
sprocket.zipfrom freeframe.dev/templates/sprocket (the WordPress tab). - 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.
- 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.
- 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:
pnpm install
pnpm dev # builds the CSS, then starts WordPress with the theme at http://127.0.0.1:9400pnpm 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.txtCustomizing#
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
fontFaceentries intheme.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.
revealfades a block in as it scrolls into view.link-cardon 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):
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,configuratorandreserve(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()inmain.js; keep a 16:10 frame. - Journal covers are drawn by the
ARTfunctions inmain.js(640 × 400);cover(p)picksART[p.art]. - Team portraits are drawn by
portrait(p)from theskin,hair,style(short,long,bun,bald,curly),shirt,glassesandbeardvalues inPEOPLE(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, sourceog-image.svg); the faviconassets/img/favicon.svgis used until you set a Site Icon in Settings > General.
Navigation, header and footer#
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 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. 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 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/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.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 | 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
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 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.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 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_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. For a real site, setKEEP_DEMO_CURRENT: falseand write real dates. - Copyright year: written by PHP in the footer.
- 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#
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:
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/:
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:
wp i18n make-pot wp-content/themes/freeframe-sprocket wp-content/themes/freeframe-sprocket/languages/freeframe-sprocket.potSave 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:
/*
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 buildafter 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. EditBATCHESindata.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 infreeframe_sprocket_page_urls()ininc/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.