Overview#
Kinetic is a website for an independent personal trainer or a small personal training studio, built as a WordPress
block theme. The fictional brand is Fenline Performance, a two-coach studio in Romsey, Cambridge (UK), run by Ffion
Beckett and Yusuf Dacre. The theme has a home page with a goal picker that recommends a programme, the next free
consultation, client progress charts, how it works, testimonials and a pricing summary with the Kickstart offer; a
programmes page filterable by goal with a comparison table; a pricing page with packs, memberships, a cost calculator,
an FAQ and the small print; a results page with six client stories and charts, filterable by goal; a training tools
page with a plan builder, a one-rep max estimator and a running pace calculator; an about page with the coaches; a
contact page with a form, map and live studio hours; a four-step booking flow with a week view and health questions;
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/kinetic.
Headings, copy, cards, lists, buttons and images are core WordPress blocks arranged in block patterns, so you edit
them in the block editor without touching code. The interactive parts (the goal picker's choices, the filter chips,
the charts, the calculators, the booking flow, the plan builder, the studio hours and the forms) work as in the HTML
version: they are small Custom HTML blocks driven by assets/js/main.js. Colours and fonts are design tokens in
theme.json: change them in Appearance > Editor > Styles and the whole site follows.
The copy uses British English, pounds sterling, 24-hour times and UK time (Europe/London). The content is general
information for a fictional studio, not medical advice. Review all health copy, especially the health questions in
the booking flow, with a qualified professional before publishing your own site.
Quick start#
Requirements: WordPress 7.1 or newer and PHP 7.4 or newer. No plugins are needed.
- Download
kinetic.zipfrom freeframe.dev/templates/kinetic (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 Kinetic.
- Go to Appearance > Kinetic setup and click Create the demo pages. This creates the Home, Programmes, Pricing, Results, Training tools, About, Contact and Book online 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 > Kinetic pages) made of section patterns. Every section is also offered on its own in the block inserter under Patterns > Kinetic.
| Page | Slug | Page pattern | Sections (pattern files in patterns/) |
|---|---|---|---|
| Home | / |
page-home.php |
home-hero (next free consult, client progress card), ticker, goal-picker (five goals and their panels), programmes-preview, results-preview (three charts), how-it-works, testimonials, pricing-preview (plans, Kickstart, health note) |
| Programmes | /programmes/ |
page-programmes.php |
programmes-hero, programmes-list (seven programmes with goal filter, ?goal=), programmes-compare (table), programmes-basics |
| Pricing | /pricing/ |
page-pricing.php |
pricing-hero, kickstart (#kickstart), packs, memberships (#memberships), calculator (#calculator), pricing-compare, pricing-faq (#faq), terms (#terms) |
| Results | /results/ |
page-results.php |
results-hero, stories (six stories with charts and goal filter, ?goal=), reviews |
| Training tools | /tools/ |
page-tools.php |
tools-hero, plan-builder (#plan), calculators (#one-rep-max, #pace), tools-cta |
| About | /about/ |
page-about.php |
about-hero, story, coaches (#ffion, #yusuf), approach, milestones, studio |
| Contact | /contact/ |
page-contact.php |
contact-hero, contact-form (form, map, studio hours, #social), privacy (#privacy) |
| Book online | /book/ |
page-book.php |
book-hero, booking (the four steps, the confirmation and the summary) |
| 404 | any missing URL | templates/404.html |
404 |
Posts use templates/single.html; the blog, archives and search results use templates/index.html,
templates/archive.html and templates/search.html. For a plain text page (a privacy policy, for example) choose the
Page with title template.
Project structure#
freeframe-kinetic/
style.css theme header (name, version, requirements, license); no styles
theme.json design tokens: palette, fonts, spacing, layout widths, button
functions.php loads the files in inc/
inc/setup.php stylesheet and script (with its strings and the portraits' URLs),
editor styles, the `js` class script, theme colour and favicon, pattern
categories
inc/icons.php registers assets/icons/*.svg for the Icon block
inc/icon-labels.php the icons' names shown in the editor
inc/block-styles.php button block styles
inc/site-setup.php Appearance > Kinetic 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/main.js studio data, hours, booking, calculators, plan builder, charts, forms...
assets/icons/ SVG icons for the Icon block
assets/img/ athlete pictograms, coach portraits, studio, map, logo, favicon, social image
assets/fonts/ Sofia Sans and Sofia Sans Extra Condensed, 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-kinetic.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.txtCustomising#
Brand and colours#
The colours are in theme.json (settings.color.palette) and in Appearance > Editor > Styles > Colors > Palette:
| Colour | Default | Used for |
|---|---|---|
| Cobalt 50 to 950 | #eef0ff to #10163f |
the brand blue: buttons and links (600), dark bands and the footer (900, 950) |
| Flare 50 to 800 | #fff3ee to #8f2a12 |
the energy colour: highlights, button shadows and stripes; 700 and 800 for text on light backgrounds |
| Sun 100 to 700 | #fff6c9 to #7a5c00 |
small highlights: today's row in the studio hours, badges |
| Mint 50 to 800 | #ecfbf4 to #0b5a40 |
"included" ticks, the open-now badge, positive outcomes |
| Ink, Body, Muted | #0f1330, #383d5a, #575c7c |
headings, body text and secondary text |
| Line, Paper, Mist | #e4e1d8, #f6f4ef, #eceae3 |
borders, the page background and quiet fills |
| White, Black | for your own blocks |
The theme's stylesheet reads these colours from the palette (bg-cobalt-600, text-flare-700...), so changing
Cobalt 600 in Styles recolours every primary button at once. To move to another brand colour, change the whole Cobalt
scale together and keep the 600 shade dark enough for white button text (4.5:1). The pictures (assets/img/*.svg),
the SVGs in Custom HTML blocks (the hero athlete, the page header panels, the 404 lane) and the chart code
(renderChart() in assets/js/main.js) carry their own hex colours: search for #2b3be0 (cobalt), #ff5a33
(flare) and #0f1330 (ink) to change them too. The browser UI colour (theme-color) is set in
freeframe_kinetic_head_meta() in inc/setup.php.
Component classes from the HTML template work on blocks too: add them in Block settings > Advanced > Additional CSS
class(es). card gives a group the white card look, card-hover lifts it on hover, tag with tag-cobalt,
tag-flare, tag-sun, tag-mint or tag-ink makes a small label, kicker is the label with a slanted flare bar
above a heading, section-title, display and card-title are the italic condensed heading styles, lead the intro
paragraph, hl (on a span inside a heading) the slanted flare highlighter, tick-list on a List block the green
ticks, note the white note with a flare edge, lanes and lanes-light the running-track stripes, slant-top and
slant-both the diagonal section edges (with [--slant:4vw] to change the angle), and text-link the underlined
bold link.
Typography#
Two fonts are bundled in assets/fonts/ and declared in theme.json (settings.typography.fontFamilies), so no
request goes to Google: Sofia Sans Extra Condensed (variable, 600 to 900, with italics; preset display) for the
big italic headlines, card titles and buttons, and Sofia Sans (variable, 400 to 900, with italics; preset sans)
for text. 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
and --sans), so keep those slugs. Condensed display faces suit the layout best; wider faces need smaller heading
sizes.
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 > Kinetic.
What the script drives, and pictures drawn in SVG, are 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 athlete picture, the track lanes, the "Next free consult" card, the small chart | pictures and the card the script fills in |
ticker |
the scrolling goal band | decorative |
goal-picker |
the five goal cards (radio buttons) | see Goal picker; the five panels are blocks |
results-preview, stories |
the charts (figure data-chart) |
see Progress charts |
programmes-list, stories |
the filter chips, the live count and the empty message | see Programme and story filters |
programmes-compare, memberships, pricing-compare |
the tables | tables with a scrolling frame on phones |
calculator |
the calculator | see Pricing calculator |
plan-builder, calculators |
the tools | see Plan builder and One-rep max and pace calculators |
booking |
the four steps, the confirmation and the summary | see Booking flow |
contact-form, footer |
the form and its thank-you message; the open-now badge and studio hours | see Forms and Studio hours and open-now status |
the page headers, 404, about-hero |
the cobalt panel, illustrated "UI" cards and portraits | decorative pictures |
header, footer |
the menu button; the toast | 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 or hidden, so the script turns classes starting with ffd- into attributes when the
page loads, and ff-hidden into hidden: ffd-goals--strength_sport on a programme card becomes
data-goals="strength sport", and ffd-goal-panel--fatloss becomes data-goal-panel="fatloss". ff-live becomes
aria-live="polite".
revealfades a block in as it scrolls into view (reveal-xslides it in from the left); a class such as[--d:80ms]delays it.link-cardon a group makes the whole group clickable: the first link inside it stretches over the group (the programme cards on the home page, the logo).
Studio data that drives the interactive features lives in constants at the top of the second part of
assets/js/main.js, as in the HTML version:
| Constant | What it holds |
|---|---|
STUDIO |
name, time zone (Europe/London), time zone label, phone, email and address (used in .ics files) |
HOURS |
opening hours by weekday (0 = Sunday, null = closed), 24-hour HH:MM strings |
CLOSED_DATES |
bank holidays and closures as YYYY-MM-DD |
COACHES |
each coach's name, short name, role, portrait and shifts (1:1 hours by weekday) |
GROUP_CLASSES, GROUP_SIZE |
small-group classes (weekday, start time, coach, name) and the class size |
SESSIONS |
bookable session types: label, minutes, price, price label, coaches, notice and whether health answers trigger a call-back |
BOOKING |
slot spacing, weeks shown and the demo "busy" share |
GOALS |
the five goal ids and labels used by the goal picker, filters, booking and plan builder |
PRICING |
calculator prices: pay per session, packs, membership rates |
RESULTS |
chart data for client stories |
FOCUS, PLAN_DAYS, SPLITS, SCHEMES, EQUIPMENT |
plan builder rules and exercises |
Visible strings in those constants are wrapped in __(), which looks them up in the translations passed from PHP
(see Translations). The studio hours also appear as text in the Custom HTML hours lists
(<dl data-hours>) in the footer and on the contact page: when you change HOURS, update those lists too. Prices
appear in the programme cards, the pricing tables, the home page and SESSIONS and PRICING: keep them in step.
Adding a coach
Add an entry to COACHES and list its id in the coaches of each session they offer (in a child theme, see
Child themes):
var COACHES = {
// ...
maya: {
name: __('Maya Lin'), short: __('Maya'), role: __('Coach'), img: asset('assets/img/coach-maya.svg'),
shifts: { 2: [['16:00', '20:30']], 4: [['16:00', '20:30']], 6: [['08:00', '12:00']] }
}
};asset() looks the path up in the portrait URLs that freeframe_kinetic_script_assets() in inc/setup.php passes
to the script: add the new file's name to its list (or give img a full URL, for example of an image in the Media
Library). Then add a radio card to step 2 in the booking pattern's Custom HTML (copy an existing coach card,
including data-coach-next="maya"), and duplicate a coach profile on the About page in List View. Link straight to a
coach with /book/?coach=maya.
Adding a programme or a client story
On the Programmes page, duplicate a programme card (an article group) in List View and change its text, picture and
links. Its class ffd-goals--… lists the goal ids it appears under, joined with underscores
(ffd-goals--strength_sport); the chip counts are worked out by the script. On the Results page, a story works the
same way. For a new chart, add an entry to RESULTS and a figure in a Custom HTML block:
<figure class="chart" data-chart="plank" data-reveal>
<div data-chart-svg></div>
</figure>plank: { label: __('Plank hold'), unit: 'reps', x: [__('Wk 1'), __('Wk 4'), __('Wk 8')], y: [20, 45, 70], better: 'up' }unit can be kg, cm, reps, steps or time (values in seconds, shown as m:ss). With better: 'down' the
axis is flipped so improvement still rises.
Adding a session type
Add it to SESSIONS and a matching radio card (name="type", same value) to step 1 in the booking pattern's
Custom HTML:
assessment: { label: __('Fitness assessment'), minutes: 60, price: 55, priceLabel: __('£55'), coaches: ['yusuf'], notice: 1440, screen: true }Images and artwork#
All artwork is SVG in assets/img/, used by Image blocks: select one and Replace it.
pose-strength.svg,pose-fatloss.svg,pose-mobility.svg,pose-sport.svgandpose-postnatal.svgare 120 × 120 pictograms. On the programme cards they are turned white with the classes[&_img]:brightness-0 [&_img]:invert, so keep them single-colour plus flare accents.coach-ffion.svgandcoach-yusuf.svgare square portraits on the About page and in the booking confirmation (COACHES[...].img). To use photos, keep a square crop of at least 480 × 480 pixels, upload them to the Media Library, replace the Image blocks and change theimgpaths in the script (a full URL works) so the booking flow shows the same picture. Keep the alt text describing the person.studio.svg(16:10) is on the About page andmap.svg(10:7) on the Contact page. You can swap the map for an embedded map, but that adds external requests and cookies.- The home hero athlete, the page header panels, the ticker and the 404 art are inline SVG in Custom HTML blocks; replace any of them with an Image block if you prefer a picture.
- The logo is
assets/img/logo.svgin an Image block next to the Fenline name in the header and footer patterns. The social image isassets/img/og-image.png(1200 × 630, sourceog-image.svg); the faviconassets/img/favicon.svgis used until you set a Site Icon in Settings > General.
Icons are Icon blocks: select one and pick another from the Kinetic collection or WordPress's own.
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 also holds the dark
announcement bar above the header (the Kickstart offer and the phone number).
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": Home, Programmes, Pricing, Results,
Training tools, About, Contact, Book online). Its first link (Home) and last link (Book online) are hidden in
the desktop header, which has the logo and its own "Book a free consult" button; keep them first and last, or remove
the .site-nav … :first-child and :last-child rules in src/theme.css. The desktop menu shows from 1280 px wide;
below that the menu button opens the mobile menu. 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. WordPress marks the current page in the menu; the script marks the "Book a free consult" button on the booking
page.
The footer's Train and Studio menus are List blocks: click a link to change it, press Enter to add one. The address,
phone number and email also appear on the contact page and in STUDIO; search for them when you change them. The
Instagram and YouTube buttons are Button blocks with the Icon only style: change their links in the block toolbar.
Features#
Goal picker#
On the home page, five radio cards (input[name="goal"], in the goal-picker pattern's Custom HTML) inside the
group with the class ffd-goal-picker show one of five panels: groups with ffd-goal-panel--strength,
ffd-goal-panel--fatloss and so on. Edit the panels as blocks. The choice is saved under the kinetic-goal key and
preselected next time; /?goal=sport also selects a goal. The panels are an aria-live="polite" region (the class
ff-live) and the cards are a native radio group (arrow keys move between goals).
Programme and story filters#
The Programmes and Results pages use the same filter. A section with ffd-filter holds chip buttons
([data-filter-value], aria-pressed, in Custom HTML) and items with ffd-goals--…. With ffd-filter-url the
choice is written to ?goal= with history.replaceState, so /programmes/?goal=postnatal opens filtered. Unknown
values fall back to "All". A status line ([data-filter-status], role="status") announces how many items are shown,
and choosing a goal also updates kinetic-goal.
Progress charts#
renderChart() in main.js draws each <figure data-chart="id"> from RESULTS as an inline SVG line chart with
grid lines, axis labels, an area fill and a highlighted final value. Each SVG has role="img" and an aria-label
summary such as "5 km time: 31:12 at Wk 1, 26:28 at Wk 12 (lower is better)." The line draws in when scrolled into
view; with reduced motion it appears immediately.
Next free consult#
The "Next free consult" card on the home page ([data-next-consult]) shows the first bookable consultation from the
booking engine, for example "Today 13:30 with Yusuf". It stays hidden if JavaScript is off.
Booking flow#
The Book online page holds a form inside [data-booking] with four steps and a confirmation; the code is
initBooking() in assets/js/main.js:
- Session: radio cards
name="type"matching the keys ofSESSIONS. - Coach:
name="coach"withany(no preference),ffionoryusuf. Coaches who don't offer the chosen session are disabled, and each card shows its next opening ([data-coach-next]). - Time: a week view (Monday to Sunday) with previous and next week buttons, for this week and the next
BOOKING.weeksAhead - 1weeks. It opens on the week of the first available time. - Your details: name, email, mobile, optional goal and notes, seven yes/no health questions and a consent box.
- Confirmation: a summary, the coach's portrait and an Add to calendar (.ics) button.
How times are built, in slotsFor():
- A 1:1 session can start every
BOOKING.slotStepminutes inside the coach'sshiftsand the studioHOURS, and must finish by the end of the shift. - It can't overlap a small-group class the coach is leading (
GROUP_CLASSES, 60 minutes). - It must start at least
noticeminutes from now (2 hours for consults, 12 hours for training), in UK time. CLOSED_DATESand closed weekdays show "Studio closed" / "Closed".groupsessions use the fixedGROUP_CLASSEStimes and show the spaces left.- With "No preference", each time goes to the first coach in the session's
coacheslist who is free. BOOKING.demoBusySharehides a stable, pseudo-random share of times so the demo looks realistic. Set it to0when you connect real availability.
URL parameters preselect choices: /book/?type=pt60&coach=yusuf&goal=sport. type must be a SESSIONS key, coach
a COACHES key that offers that session, and goal a GOALS key (it fills the goal field; otherwise the saved
kinetic-goal is used).
All dates and times are calculated and shown in STUDIO.timeZone (Europe/London), whatever the visitor's own time
zone. The .ics file stores the start and end in UTC, so calendars convert it correctly.
Accessibility: each step heading receives focus when the step opens, the progress bar marks the current step with
aria-current="step", step errors use role="alert", each day in the week view is a labelled group and every time
is a radio with a full label (for example "Tuesday, 20 October 2026, 07:30, Ffion").
Health questions#
Step 4 asks seven PAR-Q style yes/no questions (name="parq1" to parq7). They are required. As soon as they are
answered, [data-health-outcome] shows one of three messages:
| Outcome | When | Result |
|---|---|---|
clear |
All answers are no | The booking confirms as normal |
talk |
Any yes, for a session with screen: true |
The button changes to "Send call-back request" and the confirmation explains that it is a request, not a booking, with no .ics file |
consult |
Any yes, for the free consultation (screen: false) |
The booking goes ahead, with a note that the consult is the right place to talk it through |
The wording is deliberately cautious: it asks people to talk to the coach first and to check with their doctor. Keep it that way, and have a professional review the questions for your country before you publish. Health answers are special category data under UK GDPR: if you connect this form, store them securely and only as long as you need.
Pricing calculator#
The calculator pattern ([data-calculator]) has a sessions-a-week stepper (1 to 4) and a 45/60-minute toggle. For
the chosen combination it compares, per month (PRICING.weeksPerMonth = 52 / 12):
- Pay per session:
PRICING.payg[length]× sessions a month. - Session pack: the largest pack in
PRICING.packs[length]you'd use withinPRICING.packWeeks, priced per session. - Monthly membership:
PRICING.member[length][perWeek - 1]× sessions a week × 52 / 12, rounded to the pound.
The cheapest of pack and membership is marked "Best value", and the saving against pay per session is shown per month
and per year. The results list is aria-live="polite". Change prices in PRICING and in the tables on the page.
Plan builder#
[data-plan-builder] on the Training tools page builds an illustrative week from four radio groups: days a week (2
to 5), goal, equipment (gym, dumbbells, bodyweight) and session length (30, 45 or 60 minutes, showing 3, 4 or 5
exercises). Training days come from PLAN_DAYS, the session focus for each day from SPLITS[goal][days], exercises
from FOCUS (each exercise is [gym, dumbbells, bodyweight]) and the sets and reps from SCHEMES or the focus's own
scheme. Rest days are filled in automatically. The postnatal goal shows an extra safety note.
The inputs are saved under kinetic-plan and restored on the next visit. Print calls window.print() (the print
styles hide the header and footer); Reset clears the saved plan.
One-rep max and pace calculators#
[data-orm] estimates a one-rep max with the Epley formula (weight × (1 + reps / 30), or the weight itself for one
rep), rounded to 2.5 kg or 5 lb, and lists 60 to 95% training weights from ORM_TABLE. Reps are limited to 1 to 12.
[data-pace] turns a distance (5 km, 10 km, half marathon, marathon or a custom distance up to 250 km) and a finish
time into pace per km, pace per mile and speed, plus finish times for the standard distances at the same pace.
Studio hours and open-now status#
Every [data-hours] list highlights today's row, and every [data-open-status] badge shows "Open now · until 20:30"
or "Closed · opens tomorrow 06:00", based on HOURS and CLOSED_DATES in UK time (not the visitor's clock). They
refresh every minute.
Block styles and small extras#
In the block settings' Styles panel:
| Block | Styles |
|---|---|
| Button | Fill (default, the cobalt button with a flare shadow), Outline, Flare, White, Ghost, Icon only |
The classes is-large and is-small on a Button block make it the hero or compact size. Icons on buttons and links
are classes too (icon-before-calendar, icon-after-arrow-right, with icon-size-20); see src/icons.css for the
available icons.
- FAQ: the Pricing FAQ is a set of Details blocks (native
<details>, classfaq): duplicate one to add a question. The "+" square that turns into "×" is drawn by CSS (.faq > summary::afterinsrc/theme.css). - Mobile menu:
[data-menu-button]toggles#mobile-menu, updatesaria-expandedand its label, and closes on Esc (returning focus to the button), outside clicks or link clicks. - Header: gets a shadow after scrolling.
- Scroll reveals:
revealandreveal-xblocks fade in once; revealing one also reveals everything above it. Turned off with reduced motion, and nothing is hidden without JavaScript. - Ticker: the scrolling goal band on the home page is decorative (
aria-hidden) and stops with reduced motion. - Back to top: created by the last part of
main.js; delete that part and the.back-to-topstyles insrc/theme.css(thenpnpm build) to remove it.
Forms#
Every form is a demo: it validates in the browser and shows a success state, but nothing is sent, booked or stored until you connect it. The confirmation screens say so.
Validation is driven by attributes in the forms' Custom HTML, handled by validateFields() and fieldMessage() in
assets/js/main.js:
| Attribute | Rule |
|---|---|
required |
must be filled, ticked or chosen (data-required-msg for a custom message) |
type="email" |
must look like an email address |
data-phone |
at least 10 digits |
minlength |
minimum length |
data-label |
the field's name in messages |
data-validate |
opts the field into validation |
Errors appear under the field (or under the group for radio buttons), are linked with aria-describedby, set
aria-invalid and focus the first problem.
| Form | Page | Validates |
|---|---|---|
Contact ([data-contact-form]) |
Contact | name, email, optional phone, interest, reply method, message of 10+ characters, consent |
Booking ([data-booking] form) |
Book online | session, time, name, email, mobile, all seven health questions, consent |
One-rep max ([data-orm]) |
Training tools | weight and reps |
Connecting the contact form#
Use a form plugin (replace the form's Custom HTML block with the plugin's block; add the classes label, field and
btn btn-primary to its labels, fields and button), or send the form from the script (in a child theme). In the
"Contact form" part of assets/js/main.js, send it before showing the success state, for example with Formspree:
form.addEventListener('submit', function (e) {
e.preventDefault();
if (!validateFields($$('[data-validate]', form))) return;
fetch('https://formspree.io/f/your-form-id', { method: 'POST', body: new FormData(form), headers: { Accept: 'application/json' } })
.then(function (res) {
if (!res.ok) throw new Error('Send failed');
// ...the existing code that hides the form and shows [data-form-success]
})
.catch(function () { toast(__('Sorry, that didn’t send. Please call 07700 900418.')); });
});Update the success message in the contact-form pattern so it says the message was sent.
Connecting the booking flow#
Connect a real booking system rather than a form service. Many booking platforms offer an embeddable widget: replace
the booking section with it (a Custom HTML block with its embed code) and keep the rest of the site. Or keep this
flow and:
- Load real open times (through your own server, so API keys stay private) and return them from
slotsFor()in the same{ key, mins, coach }shape. SetBOOKING.demoBusyShareto0. - In the booking form's
submithandler, POST the session,state.slotand the form data to your server. - Show step 5 only after your system confirms, and change the confirmation copy to match.
The health answers are special category data under UK GDPR. Use HTTPS and a processor you have a data processing
agreement with, keep the consent box, collect only what you need and state your retention period in your privacy
notice. Don't store the answers in WordPress (for example with a plugin that saves form entries to the database)
unless your hosting meets the same rules. They are never written to localStorage or cookies; keep it that way.
Saved data (localStorage)#
| Key | Shape | Written by | Validation on read |
|---|---|---|---|
kinetic-goal |
JSON string, one of "strength", "fatloss", "mobility", "sport", "postnatal" |
Goal picker and goal filters | Ignored unless it is a string that matches a GOALS key |
kinetic-plan |
{ "days": 3, "goal": "strength", "equipment": "gym", "minutes": 45 } |
Plan builder | Every field is checked against PLAN_DAYS, GOALS, EQUIPMENT and 30/45/60; an invalid value is removed |
Nothing else is stored; booking and contact details are never saved. To reset, use Reset on the plan builder or
clear the site's storage in the browser's developer tools. Every read and write is wrapped in try/catch, so the
site works when storage is blocked.
SEO and sharing#
Page titles come from WordPress: the page title, an em dash and the site name (for example "Pricing — Fenline
Performance"); the home page shows the site name and tagline (Settings > General). 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. A studio benefits from ExerciseGym or LocalBusiness structured data (schema.org)
with your real name, address, phone and opening hours; most SEO plugins can add it. The theme doesn't include it
because it would describe a fictional studio.
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, the menu button with
aria-expanded and a label that changes with its state, native radio groups for the goal picker, session, coach, time
and health questions, filter chips with aria-pressed and a live count, live regions for the calculator, booking and
plan builder, text summaries on every chart, native details for the FAQ, form errors announced through
aria-describedby and aria-invalid, focus moved to the first error or the next step's heading, alt text or
aria-hidden on every picture, 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 and don't skip heading levels, give images alt text (or mark them
decorative), keep link text meaningful, check contrast when you change colours (flare is only used behind dark text or
as decoration), label any field you add to a form (<label for="...">) and add data-validate when it needs
checking.
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. 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-kinetic, without node_modules/) and upload it in Appearance >
Themes, or copy the folder to wp-content/themes/ on your server. Serve the site over HTTPS, especially once the
forms are connected.
Translations#
The theme is translation-ready with the text domain freeframe-kinetic. All text in patterns, templates and PHP goes
through WordPress's translation functions; the script's strings (session types, coaches, goals, chart labels,
exercises, booking lines, calculator lines, form errors and messages) are translated in PHP
(freeframe_kinetic_script_strings() in inc/setup.php). Create a translation with Loco Translate, or with WP-CLI and
Poedit:
wp i18n make-pot wp-content/themes/freeframe-kinetic wp-content/themes/freeframe-kinetic/languages/freeframe-kinetic.potSave freeframe-kinetic-cy.mo (for example) in wp-content/languages/themes/ or in the theme's languages/ folder.
Page content created by Kinetic setup is copied into your pages in the site's language at that moment; translate it
in the editor like any other content. Dates and prices in the booking flow and the calculators are formatted in
British English by the script (toLocaleDateString and toLocaleString with en-GB).
Child themes#
Use a child theme for changes you want to keep when Kinetic is updated (your hours, coaches, session types and prices
in particular). Create wp-content/themes/kinetic-child/style.css:
/*
Theme Name: Kinetic Child
Template: freeframe-kinetic
*/Templates, parts, patterns and theme.json in the child theme override the parent's files of the same name. To use
your own copy of assets/js/main.js, dequeue the parent's script (freeframe-kinetic) and enqueue yours in the
child's functions.php, keeping the inline window.freeframeKinetic strings and portrait 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 > Kinetic.
- The week view says "No times left this week". Times need
noticeminutes of warning and are limited to coach shifts. CheckCOACHES[...].shifts,HOURSandCLOSED_DATES, and remember the demo hides about 45% of times (BOOKING.demoBusyShare). - Times or the open-now badge look an hour out. The site always uses
STUDIO.timeZone. If your studio isn't in the UK, change it to your IANA zone (for exampleAmerica/Toronto) and updateSTUDIO.zoneNameand the "UK time" notes in the patterns. - A new programme or story doesn't show up under a filter. Its group needs the class
ffd-goals--<goals>with goal ids joined by underscores. See Adding a programme or a client story. - The goal picker or plan builder opens with an old choice. It's restored from
localStorage. Use Reset on the plan builder, or removekinetic-goal/kinetic-planin the browser's storage settings. - Home or Book online is missing from the desktop menu. The desktop header hides the menu's first and last links. See Navigation, header and footer.
- Kinetic 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 > Kinetic pages.
License#
MIT, which is compatible with the GPL that WordPress requires (see LICENSE.txt in the theme folder). Free for
personal and commercial use, including client work. The athlete pictograms, coach portraits, studio illustration,
map, icons, logo and social image are original to this template and included under the same license. Sofia Sans and
Sofia Sans Extra Condensed are licensed under the SIL Open Font License 1.1 (OFL.txt in each folder of
assets/fonts/). Fenline Performance, its coaches, clients, reviews and prices are fictional.