Overview#
Kinetic is a complete website template for an independent personal trainer or a small personal training studio.
The demo content is for Fenline Performance, a fictional two-coach studio in Romsey, Cambridge (UK), run by
Ffion Beckett and Yusuf Dacre. This is the Astro version: Astro 7, TypeScript and Tailwind CSS v4, with
no UI framework. Interactivity is plain TypeScript that Astro bundles, and the site builds to static HTML, CSS and
JavaScript. This guide covers version 1.0.0 (see template.json). Live demo:
freeframe.dev/templates/kinetic.
The copy uses British English, pounds sterling, 24-hour times and the Europe/London time zone. All health content
is general and carefully worded; review it with a qualified professional before you publish your own site.
Quick start#
Requirements: Node.js 24 or newer and pnpm (npm install -g pnpm, or use npm with the same script names).
pnpm install
pnpm dev # http://localhost:4321, reloads as you editOther scripts:
| Command | What it does |
|---|---|
pnpm build |
Builds the static site into dist/ |
pnpm preview |
Serves dist/ locally |
pnpm check |
astro check: type-checks .astro files and TypeScript |
pnpm typecheck |
astro sync && tsc --noEmit with TypeScript 7 |
pnpm lint |
ESLint (typescript-eslint and eslint-plugin-astro) |
Pages#
| File | Route | Purpose and notable features |
|---|---|---|
src/pages/index.astro |
/ |
Home: hero with a live "next free consult" badge, goal picker with five recommendation panels, programmes teaser, three client progress charts, how it works, testimonials, pricing teaser with the Kickstart offer and a health note |
src/pages/programmes.astro |
/programmes |
Seven programme cards filterable by goal (?goal=), what's included, prices, facts and booking links, comparison table |
src/pages/pricing.astro |
/pricing |
Kickstart intro offer (#kickstart), session packs, memberships (#memberships), cost calculator (#calculator), monthly vs pay-per-session table, FAQ (#faq), small print (#terms) |
src/pages/results.astro |
/results |
Six anonymised progress stories with SVG charts, filterable by goal (?goal=), review wall |
src/pages/tools.astro |
/tools |
Plan builder (#plan), one-rep max estimator (#one-rep-max), pace calculator (#pace) |
src/pages/about.astro |
/about |
Studio story, coach profiles (#ffion, #yusuf), approach, milestones, studio facts and safety note |
src/pages/contact.astro |
/contact |
Contact form, map, getting there, studio hours with open-now status, social links (#social), privacy note (#privacy) |
src/pages/book.astro |
/book |
Four-step booking flow with a week view, health questions and a confirmation with .ics download |
src/pages/404.astro |
404.html |
Friendly error page with links back into the site (noindex) |
Project structure#
kinetic/
astro.config.mjs site URL, base path, file-style output, Tailwind plugin
package.json scripts and dependencies
tsconfig.json extends Astro's strict preset
eslint.config.mjs typescript-eslint + eslint-plugin-astro
public/img/ favicon, social image, athlete pictograms, coach portraits, studio and map art
src/
pages/*.astro one file per page (see Pages)
layouts/Layout.astro <html>, <head> metadata, top bar, header, footer, toast and script imports
components/
TopBar.astro the dark announcement bar above the header
SiteHeader.astro logo, main navigation, "Book a free consult" button and mobile menu
SiteFooter.astro call to action, contact details, link columns, studio hours
OpeningHours.astro the studio hours list (data-hours), from HOURS_TABLE
OpenPill.astro the live open-now badge (data-open-status)
GoalChips.astro the goal filter chips on the programmes and results pages
data/
studio.ts studio details, hours, closures, coaches, group classes, sessions, goals, storage keys
pricing.ts calculator rates and the pricing page's cards, tables, FAQ and small print
results.ts chart data, client stories, testimonials and reviews
programmes.ts programme cards, comparison table, home goal picker and programme teasers
booking.ts booking page copy: session cards, coach cards, health questions
tools.ts plan-builder rules, one-rep max table, pace distances
about.ts coach profiles, values, milestones, studio facts
nav.ts header and footer links
icons.ts inline SVG icons, stars and the logo mark
lib/site.ts site name, theme colour and the url() helper
scripts/main.ts all interactivity (see Features)
scripts/back-to-top.ts the back-to-top button
styles/global.css Tailwind source: brand tokens and component classes
README.md, DOCS.md, CHANGELOG.mdCustomising#
Brand and colours#
All design tokens live in the @theme block at the top of src/styles/global.css. The ones that matter most:
| Token | Used for |
|---|---|
--color-cobalt-50 to --color-cobalt-950 |
Brand blue: buttons, links, heading accents, dark bands (900/950) |
--color-flare-50 to --color-flare-800 |
Energy colour: highlights, button shadows, stripes; 700/800 for text on light backgrounds |
--color-sun-* |
Small highlights (today's hours row) |
--color-mint-* |
Success and "included" ticks, positive outcomes |
--color-ink, --color-body, --color-muted |
Heading, body and secondary text |
--color-paper, --color-mist, --color-line |
Page background, quiet fills and borders |
To change the brand colour, replace the cobalt scale (keep 600 dark enough for white text, at least 4.5:1):
@theme {
--color-cobalt-600: #0f766e; /* buttons and links */
--color-cobalt-700: #115e59; /* hover */
--color-cobalt-950: #042f2e; /* footer and dark bands */
}A few places use hex values directly (#2b3be0 cobalt, #ff5a33 flare, #0f1330 ink): the SVG files in
public/img/, the logo in LOGO_MARK (src/data/icons.ts), the inline hero art in the pages and renderChart()
in src/scripts/main.ts. Search for them if you change the palette. The theme-color meta tag comes from
site.themeColor in src/lib/site.ts.
Typography#
Two Google Fonts are loaded in src/layouts/Layout.astro:
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sofia+Sans+Extra+Condensed:ital,wght@0,600..900;1,600..900&family=Sofia+Sans:ital,wght@0,400..900;1,400..700&display=swap" />--font-display (Sofia Sans Extra Condensed) is used by .display, .section-title, .card-title and buttons;
--font-sans (Sofia Sans) for everything else. To swap them, change the link in the layout and the two tokens in
src/styles/global.css. Condensed display faces such as Saira Extra Condensed or Barlow Condensed suit the layout
best; wider faces will need smaller heading sizes.
Content#
One-off copy (hero text, section intros, the privacy note) is markup in src/pages/*.astro. Anything repeated is
rendered with .map() from typed data in src/data/:
| File and export | What it holds |
|---|---|
studio.ts STUDIO |
Name, time zone (Europe/London), zone label, phone, email and address (used in .ics files) |
studio.ts HOURS, HOURS_TABLE |
Opening hours by weekday (0 = Sunday, null = closed) for the open-now logic, and the printed list |
studio.ts CLOSED_DATES |
Bank holidays and closures as YYYY-MM-DD |
studio.ts COACHES |
Each coach's name, short name, role, portrait and shifts (1:1 hours by weekday) |
studio.ts GROUP_CLASSES, GROUP_SIZE |
Small-group classes (weekday, start time, coach, name) and the class size |
studio.ts SESSIONS, BOOKING |
Bookable session types (label, minutes, price, coaches, notice, health screening) and booking settings |
studio.ts GOALS |
The five goal ids and labels used by the goal picker, filters, booking and plan builder |
pricing.ts PRICING |
Calculator rates: pay per session, packs, membership per-session rates |
pricing.ts HOME_PLANS, SESSION_PRICES, MEMBERSHIP_ROWS, MONTHLY_PLANS, COMPARE_ROWS, FAQ, SMALL_PRINT |
The prices and copy shown on the home and pricing pages |
results.ts RESULTS |
Chart data for client stories |
results.ts STORIES, HOME_STORIES, TESTIMONIALS, REVIEWS |
Story cards, home testimonials and the review wall |
programmes.ts PROGRAMMES, PROGRAMME_TABLE, HOME_PROGRAMMES, GOAL_PICKER, BASICS |
Programme cards, comparison table, home teasers and goal picker panels |
booking.ts SESSION_OPTIONS, COACH_OPTIONS, PARQ, GOAL_OPTIONS |
Booking page cards, health questions and goal options |
tools.ts FOCUS, PLAN_DAYS, SPLITS, SCHEMES, EQUIPMENT, ORM_TABLE, DISTANCES |
Plan builder rules and exercises, one-rep max table, race distances |
about.ts COACH_PROFILES, VALUES, MILESTONES, STUDIO_STATS, AMENITIES |
About page content |
Keep related data in step: HOURS_TABLE should match HOURS, the price copy in pricing.ts, programmes.ts and
booking.ts should match SESSIONS and PRICING, and each SESSION_OPTIONS id must be a SESSIONS key.
Add a coach. Add an entry to COACHES (and its id to the CoachId type), list them in the coaches of each
session they offer, add them to COACH_OPTIONS for the booking page and to COACH_PROFILES for the about page, and
put their portrait in public/img/:
export type CoachId = "ffion" | "yusuf" | "maya";
maya: {
name: "Maya Lin", short: "Maya", role: "Coach", img: "/img/coach-maya.svg",
shifts: { 2: [["16:00", "20:30"]], 4: [["16:00", "20:30"]], 6: [["08:00", "12:00"]] },
},Add a session type. Add it to SESSIONS (and its id to SessionId) and a card to SESSION_OPTIONS with the same
id:
assessment: { label: "Fitness assessment", minutes: 60, price: 55, priceLabel: "£55", coaches: ["yusuf"], notice: 1440, screen: true },Add a programme. Add an object to PROGRAMMES in src/data/programmes.ts. Its goals list decides which
filters it appears under; filter counts update automatically. pose picks one of the public/img/pose-*.svg
pictograms.
Add a client story chart. Add an entry to RESULTS (and its id to ChartId) and a story to STORIES with the
same chart id:
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. To place a chart elsewhere, use this markup:
<figure class="chart" data-chart="plank" data-reveal>
<div data-chart-svg></div>
</figure>Images and artwork#
All artwork is SVG in public/img/ and drawn for this template. Reference it with url()
(src={url("/img/studio.svg")}) so it works under a base path.
pose-strength.svg,pose-fatloss.svg,pose-mobility.svg,pose-sport.svgandpose-postnatal.svgare 120 x 120 pictograms. On selected goal cards and programme headers they are turned white with a CSS filter (brightness-0 invert), so keep them single-colour plus flare accents.coach-ffion.svgandcoach-yusuf.svgare square portraits. Replace them with square photos (at least 480 x 480), updateimginCOACHESand keep thealttext on the about page describing the person.studio.svgis 16:10 andmap.svgis 10:7. You can swap the map for an embedded map, but that adds external requests and cookies.- The home hero, page heroes and 404 art are inline SVG in the pages.
The social image is public/img/og-image.png (1200 x 630), rendered from og-image.svg. Edit the SVG, then export it
to PNG at 1200 x 630. The favicon is public/img/favicon.svg.
Navigation, header and footer#
The announcement bar (TopBar.astro), header (SiteHeader.astro) and footer (SiteFooter.astro) are shared by
every page through Layout.astro. Links live in src/data/nav.ts:
MAIN_NAV: the desktop navigation;MOBILE_NAVadds Home first and Book online last for the mobile menu.FOOTER_TRAINandFOOTER_STUDIO: the footer link columns.
Each page passes current to <Layout> (for example current="pricing"), and links whose page matches get
aria-current="page". The desktop navigation appears from 1280 px (xl); below that the menu button is shown.
export const MAIN_NAV: NavLink[] = [
{ label: "Programmes", href: "/programmes", page: "programmes" },
{ label: "Classes", href: "/classes", page: "classes" }, // a new page: src/pages/classes.astro
// ...
];Features#
All behaviour is in src/scripts/main.ts, imported once from Layout.astro. Each feature looks for its data
attributes, so a page without them simply skips it.
Goal picker#
On the home page, five radio cards (input[name="goal"]) inside [data-goal-picker] show one of five panels
([data-goal-panel="strength"] and so on), rendered from GOAL_PICKER. The choice is saved under the kinetic-goal
key and preselected next time; /?goal=sport also selects a goal. The panel container is an aria-live="polite"
region 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 [data-filter] wrapper holds chip buttons
([data-filter-value], aria-pressed, from GoalChips.astro) and items with data-goals. With data-filter-url
on the wrapper the choice is written to ?goal= with history.replaceState, so /programmes?goal=postnatal opens
filtered. Values that aren't GOALS keys 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() 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#
[data-next-consult] on the home page shows the first bookable consultation from the booking engine, for example
"Tomorrow 07:00 with Ffion". It stays hidden if JavaScript is off.
Booking flow#
The book page holds a form inside [data-booking] with four steps and a confirmation:
- 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, coach 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). Other values are ignored.
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, from PARQ in src/data/booking.ts).
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 | 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#
[data-calculator] on the pricing page 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]x 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]x sessions a week x 52 / 12, rounded to the pound.
The cheaper of pack and membership is marked "Best value", and the saving against pay per session is shown per month
and per year. With the defaults (2 sessions a week, 60 minutes) that is £537, £459 and £433, a saving of £104 a month.
The results list is aria-live="polite". Change prices in PRICING and in the copy in src/data/pricing.ts.
Plan builder#
[data-plan-builder] on the 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
options and defaults are PLAN_OPTIONS in src/data/tools.ts.
The inputs are saved under kinetic-plan and restored on the next visit; without a saved plan, ?goal= or the
saved kinetic-goal preselects the goal. 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 x (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.
The default 80 kg x 5 gives 92.5 kg.
[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 (PACE_TABLE) at the
same pace. The default 5 km in 27:30 gives 5:30 /km.
Studio hours and open-now status#
Every [data-hours] list (OpeningHours.astro) highlights today's row, and every [data-open-status] badge
(OpenPill.astro) shows "Open now · until 20:30" or "Closed · opens tomorrow 06:00", based on HOURS and
CLOSED_DATES in UK time. They refresh every minute.
Small extras#
- Mobile menu:
[data-menu-button]toggles#mobile-menu, updatesaria-expandedand its label, and closes on Esc, outside clicks or link clicks. - Scroll reveals:
.revealand.reveal-xelements fade in once; revealing one also reveals everything above it. Turned off with reduced motion. Thejsclass that enables them is set by an inline script in the layout's<head>, so content is never hidden when JavaScript is off. - Ticker: the scrolling goal band on the home page is decorative (
aria-hidden) and stops with reduced motion. - Back to top:
src/scripts/back-to-top.ts. Delete the file, its import inLayout.astroand the.back-to-topstyles inglobal.cssto remove it.
Forms#
There are two forms. Both validate in the browser with inline errors (linked with aria-describedby), focus the first
invalid field and don't send anything until you connect them.
| Form | Validates |
|---|---|
Contact (contact.astro, [data-contact-form]) |
Name, email format, optional phone (at least 10 digits if filled), interest, reply method, message of 10+ characters, consent |
Booking (book.astro, [data-booking] form) |
Session, time, name, email, mobile, all seven health questions, consent |
Validation rules come from HTML attributes on fields with data-validate: required, type="email", minlength,
data-phone, plus data-label and data-required-msg for the messages. /contact?goal=sport preselects the
interest.
Connecting the contact form#
To connect the contact form to Formspree (or any service that accepts a POST), give the form
an action in src/pages/contact.astro:
<form action="https://formspree.io/f/yourid" method="POST" class="mt-7 grid gap-5 sm:grid-cols-2" novalidate data-contact-form>Then, in the "Contact form" block of src/scripts/main.ts, replace the success code inside the submit handler with a
real request:
form.addEventListener("submit", async (e) => {
e.preventDefault();
if (!validateFields($$<Field>("[data-validate]", form))) return;
try {
const res = await fetch(form.action, { method: "POST", body: new FormData(form), headers: { Accept: "application/json" } });
if (!res.ok) throw new Error("Send failed");
} catch {
toast("Sorry, that didn’t send. Please call 07700 900418.");
return;
}
const done = must("[data-form-success]", parent);
must("[data-success-name]", done).textContent = valueOf(form, "name").trim().split(" ")[0];
form.hidden = true;
done.hidden = false;
done.focus();
});Update the success message so it says the message was sent. The site is static, so there's no server code to
write; if you'd rather handle the form yourself, add an Astro adapter and an API endpoint (src/pages/api/contact.ts)
and post to that instead.
Connecting the booking flow#
Connect a real booking system rather than a form service: replace slotsFor() with a call to your availability API
(and set BOOKING.demoBusyShare to 0), and in the booking submit handler POST the session, state.slot and the
form data before showing step 5. Many booking platforms offer an embed instead; you can replace the booking section
of book.astro with it and keep the rest of the site.
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 one of the GOALS keys |
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 |
The keys are GOAL_KEY and PLAN_KEY in src/data/studio.ts. 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#
Each page passes title and description to <Layout>. The layout outputs the meta description, a canonical link,
Open Graph tags (og:title, og:description, og:type, og:site_name, og:url, og:image, og:locale),
twitter:card and theme-color. 404.astro passes noindex, which outputs <meta name="robots" content="noindex">
and no canonical.
Canonical, og:url and og:image are absolute URLs built from site in astro.config.mjs. Set your domain when
building:
SITE_URL=https://www.example.co.uk pnpm buildor change the fallback (https://kinetic.example.com) in astro.config.mjs.
Also consider:
@astrojs/sitemap(or a hand-writtenpublic/sitemap.xml) listing the eight public pages, and apublic/robots.txtthat points to it;LocalBusinessorExerciseGymstructured data (JSON-LD) inindex.astrowith your real address, phone and opening hours. The template doesn't include it because it would describe a fictional studio.
Accessibility#
Built in:
- Skip link, landmarks (
header,nav,main,footer), one<h1>per page and a logical heading order. - Visible focus styles on every control; the mobile menu exposes
aria-expandedand closes with Esc. - Native radio groups for the goal picker, session, coach, time and health questions; filters use
aria-pressedbuttons; the calculator, filters, booking and plan builder announce changes with live regions. - Charts have text summaries; decorative SVG is
aria-hidden; meaningful images havealttext. - Colour contrast meets WCAG AA for text; flare (
#ff5a33) is only used behind dark text or as decoration. prefers-reduced-motionturns off reveals, the ticker, floating cards, chart drawing and smooth scrolling.
When editing, keep one h1 per page, give every new input a <label>, keep aria-labelledby and
aria-describedby pointing at ids that exist, and check contrast if you change colours.
Building and deploying#
pnpm build # static site in dist/
pnpm preview # preview dist/ locallydist/ is plain HTML, CSS and JavaScript. Pages are written as pricing.html rather than pricing/index.html
(build.format: "file" in astro.config.mjs), and links point to /pricing without a trailing slash. Netlify,
Cloudflare Pages and GitHub Pages serve /pricing from pricing.html automatically; Vercel does with cleanUrls
(below). 404.html is served for unknown URLs by most hosts.
- Netlify / Cloudflare Pages: build command
pnpm build, publish directorydist. - Vercel: import the repository; it detects Astro and static output needs no adapter. Add a
vercel.jsonwith{ "cleanUrls": true }so/pricingservespricing.html. - GitHub Pages: use the official Astro GitHub Action. For a
project site (
username.github.io/repo), build withDEMO_BASE_PATH=/repo. - Any static host: upload the contents of
dist/. If the host doesn't map/pricingtopricing.html, changebuild.formatto"directory"inastro.config.mjs.
To serve the site from a sub-folder, set DEMO_BASE_PATH when building, for example DEMO_BASE_PATH=/kinetic pnpm build. It becomes Astro's base, and every link and image written with url() includes it (including the coach
portrait the booking confirmation shows). astro.config.mjs also sets compressHTML: false, so the spaces between
words and inline tags that start on a new line are kept.
Tailwind scans src/ for class names, including the strings in src/scripts/main.ts. Classes built from strings in
TypeScript must appear in full somewhere (for example "plan-day"), not assembled from pieces.
demo/ (if present) is the build used for the freeframe.dev demo; you can delete it. Serve the site over HTTPS,
especially once forms are connected.
TypeScript 7 setup#
package.json installs two TypeScript packages on purpose. @typescript/native is TypeScript 7 and provides the
tsc that pnpm typecheck runs. typescript is an alias for @typescript/typescript6, the TypeScript 6 API that
astro check and typescript-eslint load. When those tools support TypeScript 7, replace both with a single
typescript@7. tsconfig.json extends Astro's strict preset.
Troubleshooting#
- The 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 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 pages. - 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. ?goal=links don't filter. The value must be one of theGOALSkeys (strength,fatloss,mobility,sport,postnatal). Anything else shows all items.- Links or images break when the site is in a sub-folder. Write internal links and
public/paths withurl()(href={url("/pricing")}), never as a bare"/pricing", and build withDEMO_BASE_PATH. /pricingshows a 404 on the host but/pricing.htmlworks. The host doesn't map extensionless URLs to.htmlfiles. Turn on clean URLs (VercelcleanUrls) or switchbuild.formatto"directory".- The social preview has no image. Build with
SITE_URLset to your real domain soog:imageis an absolutehttps://URL toog-image.png. - A chart is empty. The
data-chartvalue must match a key inRESULTS, and the figure needs a<div data-chart-svg>inside it.
License#
MIT. Free for personal and commercial use, including client work. The full license is in LICENSE.txt in the
download. All artwork (the athlete pictograms, portraits, studio illustration, map, icons and social image) is original
to this template and covered by the same license. Fenline Performance, its coaches, clients, reviews and prices are
fictional.