Overview#
Tempo is a website for a band or solo musician, built as a WordPress block theme. The fictional band
is Marigold Static, a four-piece indie band from the Southside of Glasgow (Isla Carrick, Reuben
Oyelaran, Fenna de Wit and Callum Shiach) whose second album, Weather for Two, is about to come out
on the fictional Lantern House Records. The theme has a home page with a release countdown, a
remembered pre-save button, the new album's track list, a click-to-play video poster, the next four
shows, a merch preview and a fan-club sign-up; a discography with a working track player; tour dates
with filters and calendar downloads; a merch store with a bag; an about page; an electronic press kit
(EPK); a contact form that routes messages by topic; and a 404 page. The look is a printed gig poster:
warm paper, black ink, fluorescent pink, cobalt and sun-yellow "riso" inks, condensed poster
headlines and SVG artwork throughout. 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/tempo.
The pages (headings, copy, the band, the timeline, quotes, contacts, buttons) are core WordPress
blocks arranged in block patterns, so you edit them in the block editor without touching code. The
music works as in the HTML version: the releases, tour dates, products and contact topics are lists
in assets/js/main.js, and the script draws the album covers, track lists, the discography, the
tour list, the merch store, the player bar and the bag from them. There are no photos: every cover,
mock-up and portrait is SVG. Colours and fonts are design tokens in theme.json: change them in
Appearance > Editor > Styles and the whole site follows. The design is light only.
Quick start#
Requirements: WordPress 7.1 or newer and PHP 7.4 or newer. No plugins are needed.
- Download
tempo.zipfrom freeframe.dev/templates/tempo (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 Tempo.
- Go to Appearance > Tempo setup and click Create the demo pages. This creates the Home, Music, Tour, Merch, About, Press kit and Contact 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.
- In Settings > General, the demo uses "Marigold Static" as the Site Title: the header and footer show it next to the logo, and the script uses it as the band's name in calendar files and the fact sheet download. Set your own.
- 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 > Tempo pages) made of section patterns. Every section is also offered on its own in the block inserter under Patterns > Tempo.
| Page | Slug | Page pattern | Sections (pattern files in patterns/) |
|---|---|---|---|
| Home | / |
page-home.php |
home-hero (#presave: countdown, pre-save, "Play Glasshouse", platform links), ticker, the-record (album notes and track list), video, next-shows, merch-preview, press-quote, fan-club (#join) |
| Music | /music/ |
page-music.php |
music-hero (release counts), releases (type filter and grid), release-detail (#release), hold-it |
| Tour | /tour/ |
page-tour.php |
tour-hero, tour-dates (#dates: filters, dates, past shows), tour-notify (#notify), tour-faq |
| Merch | /merch/ |
page-merch.php |
merch-hero, merch-products (category filter and cards), shipping-sizing |
| About | /about/ |
page-about.php |
about-hero, story, members, timeline, what-people-say |
| Press kit | /press/ |
page-press.php |
press-hero, fact-sheet (fact sheet and bio tabs), downloads (#downloads, tech rider), press-quotes, press-contacts (#contacts) |
| Contact | /contact/ |
page-contact.php |
contact-hero, contact-form (topic-routed form and direct contacts) |
| 404 | any missing URL | templates/404.html |
404 |
Links worth knowing: /music/?type=ep&release=night-bus-hymns#release,
/tour/?region=europe&country=Germany, /tour/?notify=paris#notify, /merch/?cat=apparel,
/merch/#p-glasshouse-tee and /contact/?topic=booking. The script links to the Music, Tour and
Merch pages by their slugs: if you rename one, change its URL in freeframe_tempo_page_urls() in
inc/setup.php too. 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-tempo/
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 script (with its strings and page URLs), editor styles,
the `js` class script, the paper grain body class, 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 the Dark and Ghost button styles
inc/site-setup.php Appearance > Tempo 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 band data, artwork, audio, player, pages, cart, press kit, forms
assets/icons/ SVG icons for the Icon block and the platform links
assets/img/ logo, band and member portraits, favicon, social image
assets/fonts/ Familjen Grotesk, Big Shoulders Display and Space Mono, 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-tempo.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.txtassets/js/main.js starts with a short WordPress adapter, then follows the HTML version's numbered
sections: band data, helpers, artwork, audio, player interfaces, home page, music page, tour page,
merch and cart, press kit, forms, header and reveals, back to top. Search for a section title to find
your way around.
Customising#
Brand and colours#
The colours are in theme.json (settings.color.palette) and in Appearance > Editor > Styles >
Colors > Palette:
| Colour | Default | Used for |
|---|---|---|
| Base (Paper) | #f4efe6 |
the page background |
| Paper 2, Card, Line | #ebe4d6, #fbf8f2, #d9d0bf |
alternate sections; cards, fields and the bag; hairlines on paper |
| Contrast (Ink) | #15130f |
text, borders, hard shadows and the dark sections |
| Ink 2, Ink line | #24211b, #3a352c |
raised surfaces and hairlines on ink |
| Soft, Subtle | #4b463d, #686155 |
secondary text and captions on paper |
| Fog, Haze | #bdb5a6, #9c9485 |
secondary text and captions on ink |
| Pink, Pink deep, Pink soft | #ff4d94, #c4155f, #ffc2da |
the brand: primary buttons, chips and highlights (with ink text); pink text and links on paper; chosen options |
| Cobalt, Cobalt soft | #2b3fd6, #c9cff7 |
the second ink: the fan-club section and focus rings |
| Sun | #ffcf33 |
the third ink: highlights, stickers and success boxes |
The stylesheet reads these colours from the palette (bg-pink, text-soft...), so changing Pink in
Styles recolours every primary button and chip at once. Keep Pink deep at a 4.5:1 contrast with the
paper. A few colours are written directly into SVG: the logo (assets/img/logo.svg and
logo-light.svg), the hero record, the video poster, the 404 record, the bag illustrations, the
album covers (each release's art) and the merch mock-ups in assets/js/main.js; search for
#ff4d94 to update them too.
Dark sections carry the class on-ink. Inside them buttons, links, meta text and form fields switch
to their light-on-dark versions automatically.
Typography#
Three fonts are bundled in assets/fonts/ and declared in theme.json
(settings.typography.fontFamilies), so no request goes to Google: Familjen Grotesk (variable,
400 to 700 with italics; preset familjen-grotesk) for text, Big Shoulders Display (700 to 900;
preset big-shoulders-display) for the condensed poster headlines (poster, h-hero, h-page,
h-section, font-display) and Space Mono (400 and 700; preset space-mono) for labels, dates
and prices (meta, eyebrow, chip). 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, so keep those slugs. The generated album covers name Big Shoulders
Display and Space Mono directly (font-family="Big Shoulders Display, …").
Font sizes are the presets in theme.json (xs 0.75rem to 9xl 8rem, the Tailwind scale).
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 > Tempo.
What the script draws or fills 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 rings, the countdown (data-countdown), the pre-save and play buttons, the record and cover (.hero-art) |
see New release countdown and pre-save |
ticker |
the scrolling list of tour cities | hidden from screen readers (decoration) |
the-record |
<div data-new-tracklist> |
the new album's track list |
video |
the poster (data-video) and its note |
see Video poster |
next-shows, merch-preview |
<ul data-next-shows>, <ul data-merch-preview> |
the next four shows and three products from the data |
fan-club, tour-notify |
the forms and their success boxes | see Forms |
releases, release-detail |
the type filter, the count, <ul data-release-grid>, <div data-release-detail> |
see Discography |
tour-dates |
the region filter, the country select, the count, <ul data-tour-list>, the empty state and the past shows |
see Tour dates and calendar files |
merch-products |
the category filter, the count and <ul data-merch-grid> |
see Merch and cart |
fact-sheet, downloads, press-contacts |
the word count, tabs, bio panels, copy and download buttons, the cover and logo art, the tech rider | see Press kit |
contact-form |
the form and its success box | see Forms |
404 |
the record illustration | decoration |
header, footer |
the bag and menu buttons, the big band name, the player bar, the bag drawer and the toast | driven by the script |
A few blocks carry a class (Block settings > Advanced > Additional CSS class(es)) that the script or stylesheet reads:
ffd-<name>--<value>is turned into the attributedata-<name>="<value>"when the page loads (blocks can't store data attributes):ffd-release-state--soonandffd-release-state--outon the home hero's two text groups,ffd-release-dateon the release date in the hero text andffd-facton each fact sheet row (the fact sheet download reads them). On a Button block the attribute goes on its link.ff-hiddenhides a block until the script decides to show it (the "out now" text, the mobile menu); the editor always shows it, with a dashed outline.revealfades a block in as it scrolls into view.link-cardon a group makes the whole group clickable (the 404 cards, the logo).eyebrow(mono label with a pink dot),meta(mono caption),lead,h-hero,h-page,h-section,poster,card,card-shadow,chip(withchip-pink,chip-sun,chip-ink,chip-paper),hl(the sun highlighter),stroke-text,prose-bioanddropcapare the HTML template's text and surface styles.
The band data
Everything that is listed, filtered or played is at the top of assets/js/main.js:
| Constant | What it holds |
|---|---|
BAND |
The name (the Site Title), site URL, email, currency and locale for prices and dates, freeShippingFrom, shipping, keepDemoDatesFresh |
RELEASES |
Every release, newest first: id, type (album, ep, single), title, date, label, cat, art, merch, blurb, credits, tracks |
REGIONS, TOUR |
Tour regions and shows: id, date, time (doors), city, country, region, venue, status, onSale, note, support, optional tickets |
PRODUCTS, MERCH_CATS |
Merch: id, name, cat, kind, price, badge, desc, optional release, colours, sizes, soldOut |
CONTACT_TOPICS |
Contact form topics, who each one goes to and the reply time |
Add a release by putting a new object at the start of RELEASES:
{ id: 'harbour-ep', type: 'ep', title: 'Harbour', date: '2027-05-07', label: 'Lantern House Records', cat: 'LHR034',
art: { bg: '#c9cff7', a: '#ff4d94', b: '#15130f', ink: '#15130f', motif: 'moons' },
blurb: 'Four songs recorded on a ferry.', credits: 'Produced by Ailsa Braid.',
tracks: [{ title: 'Harbour', len: '3:40', early: true }, { title: 'Tide Tables', len: '4:05' }] }The first release in the list is the "new release" the home page counts down to. A release dated in
the future shows "Out <date>"; only tracks with early: true can be played before release day. Track
ids are generated as <release id>-<track number> (for example weather-for-two-2). The home hero's
headline, copy and sticker and the the-record notes are page content: update them in the editor
when you change the new release.
Add a show to TOUR:
{ id: 'oslo', date: '2026-12-13', time: '20:00', city: 'Oslo', country: 'Norway', region: 'europe',
venue: 'Kulturhuset Sal 2', status: 'on-sale', support: 'Twin Ferries', tickets: 'https://tickets.example/oslo' }status is on-sale, low (shows "Last tickets"), sold-out (shows a waitlist link) or soon
(shows "On sale <onSale>" and a reminder link). Without tickets, ticketUrl() builds a placeholder
address. Shows dated before today move to "Past shows" automatically, above the past shows written in
the tour-dates pattern. The counts on the filter buttons are filled in by the script.
Demo dates: BAND.keepDemoDatesFresh moves the whole tour, and the new album (the release with
campaign: true), forward to the same weeks of the next year once every tour date has passed, by whole
weeks so weekdays never change (Friday 13 November 2026 becomes Friday 12 November 2027). The demo then
has an album on the way and upcoming shows again. Set it to false on your real site.
The dates written in the patterns follow RELEASES and TOUR (initDemoText()): the announcement
bar's data-release-note ("Weather for Two is out 13 November", then "is out now"), data-release-day
(the fact sheet, the bios and the about timeline, with data-release-year), data-tour-years
("2026–27", also in the tour page title) and data-tour-from ("November 2026"). Once the new album is
out, the buttons with the class ffd-release-cta--<key> (the "Pre-save" buttons, "Pre-order the LP")
and the "Hear the first single" link read their text from RELEASE_CTA instead (listen,
listen-album, listen-now, buy-lp; translated in inc/setup.php), products with the "Pre-order"
badge show "Out now", and a show marked status: 'soon' goes on sale on its onSale date.
Add a product to PRODUCTS:
{ id: 'beanie', name: 'Static beanie', cat: 'apparel', kind: 'tee', price: 20,
desc: 'Ribbed knit with a woven label.', colours: [{ name: 'Pink', hex: '#ff4d94' }],
sizes: ['One size'] }kind picks the SVG mock-up (lp, cd, cassette, tee, hoodie, poster, tote or pins);
lp and cd use the cover of the release you set. Products with more than one colour get swatches;
products with sizes require a size before they can be added. Sizes listed in soldOut are shown
crossed out and disabled. The home page's merch preview shows the products listed in initHome()
(['wft-lp', 'glasshouse-tee', 'tour-poster']).
Edit assets/js/main.js in a child theme (copy it there and load it instead, see
Child themes), or your releases, dates and products are overwritten when you update
the theme. Release, show and product names and descriptions are data, not translatable strings.
Images and artwork#
There are no photos. All artwork is SVG:
- Album covers are drawn by
cover()inassets/js/main.jsfrom each release'sartobject:bg(paper colour),aandb(two inks),ink(title colour) andmotif(weather,glass,river,signal,moons,bus,sunorgrid). Any element withdata-cover="<release id>"gets the cover painted into it (the home hero and the press kit's download card); adddata-cover-label=""when the cover is decorative. To use real artwork, return an<img src="…" alt="">string fromcover()instead (square, at least 1200×1200). - Merch mock-ups are drawn by
productArt()for the selected colour, in a 4:5 frame. Replace them with 4:5 product photos the same way. - Band and member portraits are
assets/img/band.svg(3:2, 1200×800) andassets/img/member-*.svg(4:5, 400×500) in Image blocks: select one and Replace it with a photo of the same ratio, and keep the alt text accurate. The press kit's band illustration download link points atassets/img/band.svgin its Custom HTML block. - The hero record, the video poster, the 404 record and the bag illustrations are SVG in Custom HTML blocks.
The logo is assets/img/logo.svg (and logo-light.svg in the footer) in an Image block next to
the Site Title block in the header and footer. The platform icons before the "Pre-save or listen
on" links and the arrow in "All tour dates" are drawn with CSS masks (icon-before-spindle,
icon-after-arrow-right...; artwork in src/icons.css), and the icons in the merch hero and the
fan-club list are Icon blocks: select one and pick another from the Tempo collection or
WordPress's own. The social image is assets/img/og-image.png (1200 × 630, source og-image.svg);
the favicon assets/img/favicon.svg is used until you set a Site Icon in Settings > General.
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 "New album" announcement bar; edit or delete it there.
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, Music, Tour, Merch, About, Press kit, Contact). Its first link (Home) is hidden in
the desktop header, as in the HTML version, because the logo links home; keep that order, or remove
the .site-nav … :first-child rule in src/theme.css. 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,
for both blocks.
The footer's three link columns are List blocks: click a link to change it, press Enter to add one. The copyright year is printed by PHP. The big band name, the player bar, the bag drawer and the toast are Custom HTML blocks at the end of the footer pattern; keep them on every page, because the player and the bag work everywhere.
Features#
The player#
Any button with data-track="<track id>" plays that track; data-play-release="<release id>" plays a
release from its first playable track (add data-play-track="<track id>" to start somewhere else,
and data-label-play for its label). Pressing a playing button again pauses it. Playing a track
queues the rest of its release, so previous and next work.
The player bar (#player, in the footer pattern) slides up when something is loaded. It has
previous, play/pause, next, a seek slider, mute and close. Every track list shows the current track
with an equaliser and aria-current, and play buttons switch to pause.
Previews without audio files: each track's preview is a 30-second piece of music synthesised in
the browser by Synth (a pad, bass, arpeggio and drums in a key, tempo and chord progression picked
from the track id), encoded as a WAV file and played by a normal <audio> element. To play your real
audio, upload the files to Media and give the track a src with the file's URL:
{ title: 'Glasshouse', len: '3:41', early: true, src: 'https://your-site.example/wp-content/uploads/2026/10/glasshouse-preview.mp3' }Remove the "30-second preview" labels in trackRow() and the "Preview" chip in the footer pattern's
player bar if you play full tracks. The player also sets Media Session metadata, so lock screens and
media keys work.
Keyboard: all controls are buttons and the seek slider takes arrow keys. Space or K plays and pauses, Shift+N and Shift+P skip tracks (ignored while typing or when a button or link has focus). A polite live region announces the track that starts playing.
New release countdown and pre-save#
initRelease() reads the first release in RELEASES. Before its date the hero shows the countdown
(data-countdown), the pre-save button and "Play Glasshouse"; on release day the group with the
class ffd-release-state--soon hides and the one with ffd-release-state--out shows (in the editor
both are visible; the "out" one has a dashed outline). The date in the hero text (ffd-release-date)
is written from the release's date. The pre-save button (data-presave) toggles and remembers its
state in localStorage; point it at your pre-save service by replacing it with a link, or call that
service in its click handler.
Video poster#
The video section (data-video) shows an SVG poster and loads nothing until someone presses play.
Put your embed address in data-video-embed (in the video pattern's Custom HTML) and the button
swaps the poster for an iframe:
<div data-video data-video-embed="https://www.youtube-nocookie.com/embed/VIDEO_ID" data-video-title="Glasshouse (official video)" …>With data-video-embed empty (the demo), pressing play plays the preview of the release named in
data-video-release and animates the poster.
Discography#
The Music page renders the release grid and detail from RELEASES. The filter buttons
(data-type-filter) update the grid, the live count and the ?type= parameter; any link with
data-open-release shows that release in the detail section, moves focus to its title and updates
?release=. Without ?release= the detail shows the newest album that is out. "Copy link" copies a
link straight to the release. The counts in the music-hero pattern are page content: update them
when you add releases.
Tour dates and calendar files#
The Tour page renders upcoming shows from TOUR. The region buttons (data-region) and the country
select (#tour-country) filter the list and keep ?region= and ?country= in the address; an empty
state offers a reset. "Calendar" buttons (data-ics) download an .ics file built by icsFor() with
the doors time, venue, support act and ticket link; it works in Apple Calendar, Google Calendar and
Outlook. "Waitlist" and "Remind me" link to /tour/?notify=<show id>#notify, which fills in the
notify form's town and country. The home page shows the next four shows with the same markup.
Merch and cart#
Product cards are rendered from PRODUCTS. Choosing a colour redraws the mock-up; "Add to bag"
checks that a size is chosen, adds the item (merging identical colour and size) and opens the bag. The
bag (#cart, in the footer pattern) is a modal dialog: focus moves into it and is kept there, Escape
or the backdrop closes it and focus returns to the button that opened it. It has quantity steppers
(1–10), remove buttons, the subtotal, a free-shipping meter based on BAND.freeShippingFrom and a
shipping line based on BAND.shipping. Every bag button (data-cart-open) shows the item count.
Checkout is a demo step. "Checkout" shows an order summary; "Place demo order" empties the bag and
says clearly that nothing was charged. To sell for real, replace the data-place-order branch in
initCart() with a redirect to your store, for example a Shopify cart permalink or a Stripe Payment
Link built from cart:
if (e.target.closest('[data-place-order]')) {
window.location.href = 'https://your-shop.example/cart/' + cart.map(function (l) { return l.id + ':' + l.qty; }).join(',');
}If you sell with WooCommerce instead, link the merch buttons to your shop pages and remove the
merch-products pattern's Custom HTML.
Press kit#
The bio tabs follow the ARIA tabs pattern (arrow keys, Home and End move between Short, Medium and
Long). "Copy this bio" copies the visible bio as plain paragraphs and the word count updates with the
tab. The bios are in Custom HTML blocks (#bio-short, #bio-medium, #bio-long); edit their
paragraphs there. The downloads are generated in the browser: data-download="cover" saves the new
album's cover as SVG, logo saves #logo-art, bio saves all three bios as text, facts saves every
fact sheet row (the groups with the class ffd-fact: the first paragraph is the label, the second the
value) and rider saves #rider-text. The band illustration is a plain download link to
assets/img/band.svg. Contact cards have data-copy buttons that copy an email address.
Header, menu, reveals and back to top#
The header gets a border once you scroll. Below 1024 px the menu button opens a full-height menu
(aria-expanded, Escape closes it and returns focus; "Your bag" closes it and opens the bag). Blocks
with reveal fade in as they scroll into view; with reduced motion they are visible straight away and
the ticker, equalisers and vinyl stop moving. The back-to-top button appears after one screen, lifts
itself above the player bar and toasts, and moves focus to the main content. To remove it, delete the
"Back to top" part 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 pink button with a hard shadow), Dark (ink), Ghost (and Outline) |
The classes is-large and is-small on a Button block are the HTML template's btn-lg and
btn-sm; icon-after-arrow-right adds the arrow. The FAQ answers on the Tour page are Details
blocks with the class faq; the plus that turns into a cross is drawn by CSS. Add one by duplicating
an answer in List View.
Forms#
All forms with data-validate are checked by initForms() in assets/js/main.js: required fields,
email format, minimum lengths, patterns, dates that can't be in the past and required checkboxes and
radio groups. Errors appear under each field (linked with aria-describedby), the first invalid field
gets focus, and fields re-check as you fix them. Field names in messages come from data-name;
checkbox and radio messages from data-required-msg. On success the form is replaced by its success
box (data-success="<id>"), which takes focus.
| Form | Where | Checks |
|---|---|---|
#join-form (Static Club) |
Home (fan-club) |
Region, email, consent checkbox |
#notify-form (tour alerts) |
Tour (tour-notify) |
Email, town or city, country, travel distance |
#contact-form |
Contact (contact-form) |
Topic, name, email, message (30+ characters, with a live counter) and the topic's own fields |
Contact routing: choosing a topic shows that topic's fields (data-topic-fields) and says who
the message goes to, from CONTACT_TOPICS. Booking is routed by the "Where is the show?" select:
North America goes to the US agent, everything else to the UK and Europe agent. Fields marked
data-required are only required while their topic is selected; dates marked data-future can't be
in the past. /contact/?topic=booking preselects a topic (the footer's Booking and Sync links use
it).
Nothing is sent, and the success messages say so. To collect submissions, use a form plugin
(replace the form's Custom HTML block with the plugin's block; add the classes field,
field-label and btn-primary to its fields, labels and button), or post the form in onValid() in
section 11 of assets/js/main.js (in a child theme), for example to Formspree:
function onValid(form) {
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();
// then show the success box as before
}).catch(function () { toast('Sorry, that didn’t send. Please email us instead.'); });
}Use a different endpoint per form by checking form.id, then change the success text in the
patterns to say the message was received. For the fan-club and tour-alert forms you can post to your
mailing list provider's form endpoint instead. Every field has a name; keep one on any field you
add.
Saved data (localStorage)#
| Key | Shape | Notes |
|---|---|---|
tempo-cart |
[{ "id": "glasshouse-tee", "colour": "Pink", "size": "L", "qty": 2 }] |
Read by readCart(), which drops unknown products, colours or sizes, sold-out sizes and bad quantities, and caps quantities at 10 |
tempo-presave |
true or false |
The pre-save button's state |
Values are read with readJSON(), which falls back to defaults on missing or broken data, and
written with writeJSON(), which ignores storage errors. To reset, run
localStorage.removeItem('tempo-cart') (or tempo-presave) in the browser console, or clear the
site's data. "Place demo order" also empties the bag.
SEO and sharing#
Page titles come from WordPress: the page title, an em dash and the site name (for example
"Tour — Marigold Static"); 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. For richer
search results you can add MusicGroup and MusicEvent structured data built from your band details
and tour dates (some SEO plugins let you add custom schema). Releases and shows are drawn by
JavaScript; if search visibility for each release matters, give each one a page or post of its own.
Accessibility#
Built in: a skip link to the main content (added by WordPress, styled by the theme), landmarks
(header, labelled navigation, main, footer, the labelled player bar), one h1 per page, labelled
sections, visible focus rings (cobalt on paper, yellow on ink), aria-current="page" on the current
menu item, the menu button with aria-expanded (Escape closes the menu), filters as toggle buttons
with aria-pressed and live result counts, track buttons that say "Play" or "Pause" with the track
name, a player that announces the new track, the bag as a modal dialog with focus trapping and
Escape, the press bios as ARIA tabs, FAQ answers in <details>, form errors announced through
aria-describedby and aria-invalid, success boxes that receive focus, 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 to a form (<label for="...">) and check the contrast of any
new colour pairs.
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-tempo, 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-tempo. All text in patterns,
templates and PHP goes through WordPress's translation functions; the script's strings (player
labels, tour and bag text, release types and regions, contact routes, toasts and form messages) are
translated in PHP (freeframe_tempo_script_strings() in inc/setup.php). Release, show and product
data are in assets/js/main.js, and prices and dates are formatted with BAND.locale (en-GB).
Create a translation with Loco Translate, or with WP-CLI and Poedit:
wp i18n make-pot wp-content/themes/freeframe-tempo wp-content/themes/freeframe-tempo/languages/freeframe-tempo.potSave freeframe-tempo-de_DE.mo (for example) in wp-content/languages/themes/ or in the theme's
languages/ folder. Page content created by Tempo setup is copied into your pages in the site's
language at that moment; translate it in the editor like any other content.
Child themes#
Use a child theme for changes you want to keep when Tempo is updated (your releases, dates and
products in particular). Create wp-content/themes/tempo-child/style.css:
/*
Theme Name: Tempo Child
Template: freeframe-tempo
*/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-tempo) and enqueue yours in the child's functions.php, keeping the inline
freeframeTempoL10n and freeframeTempoPages objects (freeframe_tempo_script_strings(),
freeframe_tempo_page_urls()). 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 > Tempo.
- Release, merch or waitlist links go to a page that doesn't exist. The script links to
/music/,/tour/and/merch/. If you renamed those pages, updatefreeframe_tempo_page_urls()ininc/setup.php. - No sound when I press play. Check the tab isn't muted and the player's mute button isn't on. If
you added a
src, check the URL opens in the browser and is a format the browser plays (MP3, AAC or Ogg); a toast says when a file can't be loaded. - The tour list is empty. Every date is in the past. Add new shows, or leave
BAND.keepDemoDatesFreshon while you build the site. - Copy buttons say "Select the text and copy it". The clipboard API needs
https://(orlocalhost). - Home is missing from the desktop menu. The desktop header hides the menu's first link. See Navigation, header and footer.
- Tempo 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 > Tempo 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. The album covers, merch mock-ups, portraits, record
and video artwork, logo and social image are included under the same license. Familjen Grotesk, Big
Shoulders Display and Space Mono are licensed under the SIL Open Font License 1.1 (OFL.txt in each
folder of assets/fonts/).