Freeframe

Documentation

Solstice

v1.0.2Updated Oct 17, 2026A sunny, poster-style festival website with a line-up, timetable planner and ticket shop.

Overview#

Solstice is a sunny, poster-style website for an outdoor food and music festival, built as a WordPress block theme. The demo event is Solstice 2027, a fictional three-day festival at Ashton Court Estate in Bristol. The theme has a home page with a live countdown, a poster line-up, ticket highlights and a line-up alerts form, a filterable line-up with artist pop-ups, a multi-stage timetable where visitors build their own plan (with clash warnings and a calendar export), a ticket shop with early-bird prices, a group discount and a demo checkout, a food village with dietary filters and a map, a "Plan your visit" page with an interactive site map and a searchable FAQ, a "Get involved" page whose contact form routes enquiries by topic, 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/solstice.

Everything you see is made of core WordPress blocks arranged in block patterns, so you edit text, links, buttons, lists and images in the block editor without touching code. Colours, the fonts and font sizes are design tokens in theme.json: change them in Appearance > Editor > Styles and the whole site follows. Artists, stages, food traders and ticket prices used by the scripts are data in assets/js/data.js (see Festival data).

Quick start#

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

  1. Download solstice.zip from freeframe.dev/templates/solstice (the WordPress tab).
  2. In your WordPress admin go to Appearance > Themes > Add New Theme > Upload Theme, choose the zip and click Install Now, then Activate. The theme is listed as Freeframe Solstice.
  3. Go to Appearance > Solstice setup and click Create the demo pages. This creates the pages below from the theme's page patterns, sets Home as the front page (Settings > Reading) and creates the header menu. Pages that already exist with the same slug are left alone, so it is safe to click it again.
  4. Set the site title to your festival's name in Settings > General (the demo uses "Solstice 2027"), then edit any page in Pages, and the header, footer and templates in Appearance > Editor.

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

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

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

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

Pages#

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

Page Slug Page pattern Sections (pattern files in patterns/)
Home / page-home.php home-hero (countdown, early-bird sticker), tape (scrolling ticker), poster-lineup, whats-on, tickets-teaser, numbers (count-up), faq-teaser, alerts (sign-up form)
Line-up /lineup/ page-lineup.php lineup-hero, lineup (filters and artist cards), artist-dialog
Timetable /schedule/ page-schedule.php schedule-hero, timetable (day tabs, grid and list views, My plan), opening-times
Tickets /tickets/ page-tickets.php tickets-hero, ticket-shop (steppers, order summary, checkout, confirmation), tickets-good-to-know
Food & drink /food/ page-food.php food-hero, food-village (filters, trader cards, map), green-food
Plan your visit /info/ page-info.php info-hero, site-map (map layers), getting-there, access-families, what-to-bring, info-faq (searchable FAQ)
Get involved /contact/ page-contact.php contact-hero, contact (form with topic routing, contact details)
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.

Project structure#

freeframe-solstice/
  style.css                 theme header (name, version, requirements, license); no styles
  theme.json                design tokens: palette, gradient, fonts, font sizes, spacing, layout, button
  functions.php             loads the files in inc/
  inc/setup.php             stylesheet and scripts, editor styles, the "js" class script, script strings
                            and settings, 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/navigation.php        current-page markers (aria-current) in the header
  inc/site-setup.php        Appearance > Solstice setup: creates the demo pages and the menu
  templates/                page.html (pages), page-with-title.html, single.html, index.html,
                            archive.html, search.html, 404.html
  parts/                    header.html and footer.html (they show the header and footer patterns)
  patterns/                 every section and page (PHP files with block markup)
  assets/css/theme.css      the compiled stylesheet (do not edit: built from src/)
  assets/css/editor.css     the same for the block editor (generated by src/editor-css.mjs)
  assets/js/data.js         the festival data: days, stages, genres, artists, traders, ticket prices
  assets/js/main.js         the site's JavaScript
  assets/js/editor.js       block editor tweaks
  assets/icons/             SVG icons for the Icon block
  assets/img/               hero sun, hills and bunting, logo, favicon, social image
  assets/fonts/             the DM Sans, Bricolage Grotesque and Shrikhand font files and 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-solstice.pot, the template for translations
  blueprint.json            WordPress Playground setup used by `pnpm dev`
  screenshot.png            the picture shown in Appearance > Themes
  package.json              the CSS build and the local WordPress (Node.js tooling)
  readme.txt, README.md, DOCS.md, CHANGELOG.md, LICENSE.txt

Customising#

Brand and colours#

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

Colours Used for
Base (cream), Paper, Sand the page background, cards and fields, map paths
Contrast (ink), Muted text, outlines and the hard poster shadows; secondary text
Sun, Sun soft the main accent: sun buttons, planned sets, highlights, hovers
Tangerine, Tomato, Peach buttons (Tomato is the primary button), eyebrows, clash warnings, cards
Sea, Sky, Leaf, Grass cards, stage and diet tags, the map
White, Black plain colours for your own blocks

The theme's stylesheet reads these colours from the palette, so changing Tomato in Styles changes every primary button, eyebrow and clash warning. The Sunrise gradient preset (Styles > Colors > Gradients) is the yellow-to-orange sky of the home page hero. A few things have their colours written in: the hero gradients (bg-[linear-gradient(...)] classes in the home-hero and 404 patterns), the SVG artwork (hero sun, hills, bunting, the maps and the artists' generated art in assets/js/main.js), the diet tag colours (.diet-* in src/theme.css) and the browser bar colour (freeframe_solstice_head_meta() in inc/setup.php).

Typography#

Headlines use Bricolage Grotesque (the display, h-section, bill-* and font-display classes, condensed with font-stretch: 75%; variable, weights 400 to 800, widths 75% to 100%), the "Solstice" wordmark uses Shrikhand (wordmark class) and text uses DM Sans (variable, weights 400 to 800). They are bundled in assets/fonts/ and declared in theme.json (settings.typography.fontFamilies), so no request goes to Google. To use other fonts, install them in Appearance > Editor > Styles > Typography > Fonts (the Font Library) and select them, or replace the fontFace entries in theme.json; the stylesheet reads --wp--preset--font-family--bricolage-grotesque, --wp--preset--font-family--shrikhand and --wp--preset--font-family--dm-sans, so rename those slugs only together with src/theme.css.

Font sizes are the presets in theme.json (xs 0.75rem to 9xl 8rem, the Tailwind scale). The stylesheet uses them too: changing Large in Styles changes every text-lg in the design.

Content#

Edit pages in Pages: click any text to change it, select a button to change its link, select an image and use Replace. 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 > Solstice.

Some content is in Custom HTML blocks, because blocks can't express it or a script drives it: edit those in the block's HTML view.

Pattern Custom HTML What it is
home-hero the countdown, the early-bird sticker see Countdown and Early-bird prices
tape the scrolling ticker a looping list of what's on (the second half repeats the first for the loop)
faq-teaser, food-hero the visitor guide map picture, the food stall picture SVG artwork; replace with an Image block
lineup the filters, the count, the artist grid and the empty state the grid is drawn by the script from data.js
artist-dialog the artist pop-up filled in by the script
timetable the day tabs, views, the timetable and My plan see Timetable
ticket-shop the progress steps, each ticket row, the checkout form, the confirmation and the order summary see Tickets and checkout
food-village the filters, the trader count, the map, the trader list and the empty state see Food village
site-map the layer buttons and the map see Site map layers
info-faq the search box and the empty state see FAQ search
what-to-bring the two list headings with an icon a heading with an icon badge
alerts, contact the forms see Forms
header, footer the "My plan" links, the menu button; the wavy edge and the toast driven by assets/js/main.js

A few blocks carry a class (Block settings > Advanced > Additional CSS class(es)) that the script or stylesheet reads:

  • reveal fades a block in as it scrolls into view; a class such as [--d:80ms] delays it.
  • link-card on a group makes the whole group clickable: the first link inside it stretches over the group (the ticket highlights and the visitor guide card on the home page).
  • ffd-<name> and ffd-<name>--<value> become the data attribute data-<name>="<value>" when the page loads, and ff-hidden becomes the hidden attribute (blocks can't store either). The script uses them: ffd-early-only / ffd-early-hide on the early-bird texts, ffd-countdown-done on the "gates are open" message, ffd-count-to--40 on a number that counts up, ffd-faq, ffd-faq-a and ffd-faq-group on the searchable FAQ, and ffd-lineup, ffd-schedule, ffd-tickets, ffd-step-basket, ffd-checkout-layout, ffd-food, ffd-contact on the groups that hold each page's interactive parts. Keep them when you edit those groups.
  • ff-bill-item on each name in the poster line-up's middle rows: CSS draws the dot between names.
  • ff-faq-sm on the site map's "Read the map as a list" uses a smaller round icon.

To change a price shown in a block (the ticket highlights on the home page, the sticker), change it in data.js: the script replaces every data-price-of / data-full-of price with the current one.

Festival data#

Festival content is split between assets/js/data.js (used by the scripts) and the blocks (what visitors see first, and without JavaScript). data.js defines window.SOLSTICE:

Key Contents
keepDemoCurrent true in the demo: once the festival is over, every date moves on a year (see Demo dates). Set it to false when you enter your real dates
gatesOpen, utcOffset When the gates open and the festival's UTC offset (+01:00, British summer time)
days { id: 'fri', label: __('Friday'), short: __('Fri'), date: '2027-06-25', gates, curfew }
stages { id: 'sunhouse', name: __('Sunhouse Stage'), blurb }
genres Genre ids and labels
artists { id, name, day, stage, start, end, genre, tier, from, bio, art: [motif, background, foreground] }
diets, vendors Dietary labels; traders { id, name, kind, cuisine, dish, price, diets, pin: [x, y] }
currency, earlyBirdEnds, groupSize, groupDiscount, fee, maxPerType Ticket rules
tickets, addons { id, name, price, early, note } (add-ons have no early price)

__() translates the texts (see Translations): when you change or add one, add it to freeframe_solstice_script_strings() in inc/setup.php too if the site is translated.

Content Comes from
Line-up cards, artist pop-ups, timetable, My plan, calendar export SOLSTICE.artists, days, stages, genres
Poster line-up on the home page, "Opening times" blocks
Trader cards and map pins SOLSTICE.vendors, diets
Ticket and add-on prices everywhere, early-bird end, group discount, fee SOLSTICE.tickets, addons and the ticket rules
Ticket and add-on names and descriptions in the shop the ticket rows' Custom HTML (keep them in step with data.js)
Countdown target data-countdown in the home page's countdown

Adding an artist is one line in SOLSTICE.artists with a unique id; the line-up, timetable and plan pick it up. Times are festival-local 24-hour "HH:MM". art chooses the generated poster art: a motif (sun, waves, rings, stripes, burst, hills) and two colours. Update the poster line-up on the home page by hand.

js
{ id: 'night-swim', name: __('Night Swim'), day: 'sat', stage: 'firefly', start: '22:45', end: '23:30',
  genre: 'electronic', tier: 3, from: __('Bath'), bio: __('Ambient techno for the last dance.'),
  art: ['waves', '#24122e', '#8fd0ee'] },

Adding a trader is one line in SOLSTICE.vendors; pin is its position on the food village map in percent ([50, 44] is the middle). Adding a ticket needs a line in SOLSTICE.tickets and a ticket row in the shop's Custom HTML with data-qty="<id>" (copy a neighbouring row). Make changes to assets/js/ in a child theme (see Child themes) so a theme update doesn't overwrite them.

Images and artwork#

  • Hero art: the sun, rolling hills and bunting are SVG files in assets/img/ (hero-sun.svg, hero-hills.svg, bunting.svg) shown with Image blocks behind the page headers. Select one to replace or delete it.
  • Artist art is drawn by the script from each artist's art setting (square). To use photos, add an image URL to each artist and change artSvg() in assets/js/main.js to return an <img>.
  • Maps: the site map, the food village map and the visitor guide picture are SVG artwork in Custom HTML blocks. Replace them with an Image block (your own map) or keep editing the SVG: every map symbol has a <title> tooltip.
  • Icons: icons are Icon blocks. Select one and pick another icon from the Solstice collection (the theme's icons) or WordPress's own. Icons inside buttons and links (the arrows) are drawn by CSS: the class icon-after-arrow-right adds one; the available names are in src/icons.css.
  • Logo: the header and footer show assets/img/logo.svg in an Image block next to the "Solstice" wordmark. Edit both in Appearance > Editor > Patterns > Header (and Footer).
  • Social image: assets/img/og-image.png (1200 × 630, source og-image.svg). WordPress doesn't output Open Graph tags by itself; see SEO and sharing.
  • Favicon: assets/img/favicon.svg is used until you set a Site Icon in Settings > General.

The header and footer are template parts (parts/header.html, parts/footer.html) showing the header and footer patterns. Edit them in Appearance > Editor > Patterns > Header / Footer.

The header has two Navigation blocks showing the same menu: the desktop one and the full-screen one in the mobile menu. Edit the menu once in Appearance > Editor > Navigation (the setup creates "Main menu" with Home, Line-up, Timetable, Food & drink, Plan your visit, Get involved and Tickets). The Home and Tickets links have the class ff-mobile-only (link settings > Advanced), so only the mobile menu shows them, as in the HTML version. 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's link (aria-current="page"). The theme also marks the header's "Tickets" button on the tickets page (the button has the class ff-mark-current, see inc/navigation.php). The "My plan" links show the number of saved sets (data-plan-count).

The footer's link columns are List blocks: click a link to change it, press Enter to add one. The social links are Button blocks with the Icon only style and a class icon-before-instagram, icon-before-tiktok or icon-before-spotify; their text is read by screen readers. The copyright line names the festival's year, not the current one: edit it there.

Features#

Countdown#

The countdown on the home page (data-countdown="2027-06-25T14:00:00+01:00", Custom HTML) counts down days, hours, minutes and seconds into its [data-unit] cells and has role="timer". At zero the cells are hidden and the paragraph with ffd-countdown-done ("The gates are open…") is shown.

Early-bird prices#

earlyBirdEnds in data.js decides which price is used everywhere. Before that moment, early-bird prices show with the full price struck through and the ffd-early-only blocks (and the hero sticker) are shown; afterwards, full prices show and the ffd-early-hide blocks appear instead.

Line-up filters and artist pop-up#

The code is in the "line-up page" section of main.js. Day buttons ([data-day-filter]), the stage and genre selects (#filter-stage, #filter-genre) and "My plan only" ([data-plan-only]) filter the cards, grouped by day; the result count is announced through an aria-live region. Filters are written back to the URL, so /lineup/?day=sat&stage=meadow&genre=indie can be shared or bookmarked; /lineup/?artist=<id> opens that artist's pop-up on load.

Clicking an artist name opens a native <dialog> ([data-artist-modal]) with the artwork, stage, set time, bio, a clash warning if relevant, "Add to my plan" and "See in timetable". Escape, the close button or a click on the backdrop closes it, and focus returns to the card.

My plan and clash detection#

Every sun button ([data-plan-toggle="<artist id>"]) adds or removes a set from the visitor's plan, uses aria-pressed and an "Add … to my plan" / "Remove … from my plan" label, and shows a toast. The plan is saved in the browser (see Saved data), shared by all pages and synced between open tabs. clashes() in main.js marks two planned sets as clashing when they are on the same day and their times overlap.

Timetable#

The timetable shows one day at a time with keyboard-friendly tabs (Left/Right, Home/End). The day is kept in the URL (/schedule/?day=sun); without it, the page opens on the current festival day or Friday. "Grid" draws the four stages as columns on an hour scale (it scrolls sideways on small screens); "List" shows the day's sets in time order and is the default below 768px. Planned sets are yellow and clashes get a red striped background and a "Clash" badge. Links like /schedule/?day=fri#set-halcyon-bloom scroll to and highlight one set.

The My plan panel lists saved sets by day with their clashes, and has two buttons:

  • Add to calendar (.ics) downloads solstice-2027-my-plan.ics with one event per set (times in UTC, location and description included). It works with Apple, Google and Outlook calendars.
  • Clear plan needs two clicks within three seconds, so a stray tap can't wipe the plan.

To change the hour height of the grid, edit --hour on .tt in src/theme.css and run pnpm build.

Tickets and checkout#

Steppers ([data-qty] with [data-step="-1"] and [data-step="1"]) change quantities from 0 to maxPerType. The order summary lists every line, then:

  • Group discount: groupDiscount (10%) off the ticket subtotal when an order has groupSize (6) or more tickets. Add-ons are not discounted. A hint shows how many more tickets unlock it.
  • Booking fee: fee (£1.75) per ticket; add-ons have no fee.
  • Rules: a campervan pitch needs a Weekend + camping or Family weekend ticket per van, and a bell tent needs at least one of those tickets. The summary explains the problem and disables "Continue".

The basket is saved in the browser. "Continue to details" shows step 2, a validated form (name, email, UK postcode, two required checkboxes); a valid submit shows step 3 with a random demo order reference and the total, and empties the basket. In step 3 the order summary hides and the confirmation spans the full width, centred: the script sets data-step="done" on the group with the class ffd-checkout-layout, whose lg:data-[step=done]:grid-cols-1 class drops the summary column. Nothing is charged or sent. To sell real tickets, link the "Continue" button to your ticketing provider, or send the basket to your own checkout from the solstice:valid listener in the "tickets page" section of main.js.

Food village filters and map#

"Show" buttons ([data-kind]) filter by food, sweet or drink; dietary chips ([data-diet]) can be combined and traders must match all of them; the search box matches name, cuisine and dish. Matching pins on the map stay bright, others fade. "Show on map" highlights a trader's pin; clicking a pin (or pressing Enter on it) highlights the trader's card. /food/?diet=vegan,gluten-free preselects dietary filters.

Site map layers#

Each layer of the site map is a <g class="map-layer" data-layer-group="..."> (stages, food, toilets, firstaid, water, access). The buttons with the same data-layer toggle it with aria-pressed, and "Show all" turns every layer back on. Each symbol has a <title> tooltip, and the "Read the map as a list" Details block gives the same information as text.

Typing in the search box ([data-faq-search]) filters the questions on the Plan your visit page, opens matches, highlights the words with <mark>, hides empty groups and announces the count. The questions are Details blocks with the classes faq … ffd-faq; the question text is wrapped in <span data-faq-q> and the answer is a group with ffd-faq-a. To add a question, duplicate one (select it, then Options > Duplicate) and edit the text.

Contact routing#

The topic radios on the Get involved page show the matching [data-topic-panel] (volunteer, trader, press, general), disable the fields of the others (so they are neither validated nor submitted) and say where the message goes. /contact/?topic=trader preselects a topic. Team names and addresses are in ROUTES in the "contact" section of main.js.

Scroll reveals, tape and back to top#

Blocks with reveal fade in as they scroll into view, and everything above them is revealed too, so fast jumps never leave gaps. The home page tape scrolls continuously and pauses on hover. Numbers with ffd-count-to--<n> count up once. A back-to-top button appears after one screen of scrolling; it steps aside for the sticky order summary (data-back-to-top-hide). All motion is switched off for visitors who prefer reduced motion. The menu button (data-menu-toggle) opens a full-screen menu, moves focus to its first link, updates aria-expanded and its label, and closes with Escape.

Demo dates#

The demo is written for 25–27 June 2027. While keepDemoCurrent in assets/js/data.js is true and that edition is over (after the last day's curfew), the script moves every date to the same week of the next year, by whole weeks so weekdays never change (Friday 25 June 2027 becomes Friday 23 June 2028), so the countdown, early-bird prices, timetable and calendar export always point at an upcoming festival. The year of the edition follows. It updates the days dates, gatesOpen, earlyBirdEnds, the countdown target and the artist bios, and the text of these spans in the patterns: data-edition (dates "25–27 June 2027", dates-noyear, dates-long "Friday 25 – Sunday 27 June 2027", dates-long-noyear, year, year+1), data-date="2027-06-01" (data-date-format="dm" leaves out the year, data-date-end makes a range) and data-day-label="fri", plus the page title, aria-labels and data-error messages. Shifted dates are written in English. Before the edition is over nothing is changed, so ?now=2027-06-26T21:40 still previews the festival weekend. Set keepDemoCurrent to false once you enter your real dates.

Previewing a date#

Add ?now=YYYY-MM-DDTHH:MM (festival local time) to any page to pretend it is that moment, for example /tickets/?now=2027-03-01T10:00 to see full prices after the early-bird ends, /?now=2027-06-25T13:59 for the last minute of the countdown, or /schedule/?now=2027-06-26T15:00 to open on Saturday.

Block styles#

In the block settings' Styles panel:

Block Styles
Button Fill (default, Tomato), Ghost, Sun, Ink, Icon only (WordPress's own Outline style is replaced by Ghost)

Add the class is-large or is-small to a Button block for the large or small size.

Forms#

Every form with data-validate gets inline validation from the "forms" section of main.js: errors appear under each field (linked with aria-describedby), aria-invalid is set, and focus moves to the first problem. Groups of checkboxes or radios use <fieldset data-group-required="message">.

Form Where Validates
Line-up alerts (data-form="alerts") Home page (alerts pattern) Email, at least one alert type
Checkout (data-form="checkout") Tickets page (ticket-shop pattern) Name, email, UK postcode pattern, age and terms checkboxes
Contact (data-form="contact") Get involved page (contact pattern) Topic, name, email, topic fields, message of 20+ characters

On a valid submit the form fires a solstice:valid event with the FormData and shows its [data-success] message. Nothing is sent anywhere until you connect the forms.

Option 1: a form plugin#

For the alerts and contact forms, install a form plugin (WPForms, Gravity Forms, Fluent Forms, Contact Form 7, Jetpack Forms...) and replace the form's Custom HTML block with the plugin's block or shortcode. Add the theme's classes where the plugin allows: field on fields, label on labels, btn btn-primary on the submit button. For line-up alerts, your newsletter provider's sign-up form or plugin works the same way.

Option 2: a form service#

In the "forms" section of assets/js/main.js, find the comment "To send for real" and post the data before showing success:

js
fetch('https://formspree.io/f/your-id', {
  method: 'POST',
  body: new FormData(form),
  headers: { Accept: 'application/json' }
}).then(function (res) {
  if (!res.ok) throw new Error('Send failed');
  // show the success message here
});

Update the success texts in the form's Custom HTML (and the data-success-detail text set in the contact section of main.js) so they no longer say "Demo only". Make script changes in a child theme (copy assets/js/main.js there and load it instead), or they are overwritten when you update the theme.

For real ticket sales, don't collect payment details in this form. Use a ticketing or payment provider (Skiddle, Eventbrite, Stripe Checkout…): replace the checkout with their embed, or send the basket to their checkout from the Continue to details button. WooCommerce or an event plugin can also sell tickets in WordPress itself.

Saved data (localStorage)#

Key Shape Validation
solstice-plan Array of artist ids, e.g. ["halcyon-bloom","kemi-sunday"] Non-strings, duplicates and unknown ids are dropped and storage is repaired
solstice-cart { "tickets": { "camping": 2 }, "addons": { "parking": 1 } } Unknown ids are dropped, quantities are clamped to 0–maxPerType; removed when the basket is empty

If storage is unavailable (private mode), everything still works for the current page. Visitors can reset by clearing the plan on the timetable or the site data in their browser; in code, call localStorage.removeItem('solstice-plan'). For a new edition, change PLAN_KEY and CART_KEY in assets/js/main.js (for example solstice-2028-plan) so old plans don't carry over.

SEO and sharing#

Page titles come from WordPress: the page title, an em dash and the site name (for example "Timetable — Solstice 2027"); 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. Add Festival structured data (name, dates, location, offers) so search engines can show the event. WordPress already generates a sitemap at /wp-sitemap.xml.

Accessibility#

Built in: a skip link to the main content (added by WordPress, styled by the theme), labelled landmarks, one h1 per page, thick ink focus rings, day tabs following the tabs pattern, filters, layers and sun buttons with aria-pressed, a native artist dialog with focus return, the countdown with role="timer", counts and results announced with aria-live, map symbols with tooltips and a text version of the site map, keyboard-operable map pins, stage and diet tags with text labels (not colour alone), toasts announced with aria-live, form errors linked to their fields, and reduced-motion support for reveals, the tape, the dialog and counters. The demo passes axe (WCAG 2.1 A and AA).

When you edit: keep text on the bright colours dark (Ink) for contrast, give images alt text (or mark them decorative), label every new form field and keep one Heading 1 per page.

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/ (including the classes inside the HTML the script writes):

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

Run pnpm build after adding a utility class to a pattern or template, 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-solstice, 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-solstice. All text in patterns, templates and PHP goes through WordPress's translation functions; the scripts' strings (artists, stages, traders, tickets, labels, form errors) are translated in PHP (freeframe_solstice_script_strings() in inc/setup.php). Create a translation with Loco Translate, or with WP-CLI and Poedit:

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

Save freeframe-solstice-de_DE.mo (for example) in wp-content/languages/themes/ or in the theme's languages/ folder. Page content created by Solstice setup is copied into your pages in the site's language at that moment; translate it in the editor like any other content. Prices are formatted in pounds (en-GB) by gbp() in main.js; dates in the line-up read "Friday 25 June".

Child themes#

Use a child theme for changes you want to keep when Solstice is updated. Create wp-content/themes/solstice-child/style.css:

css
/*
Theme Name: Solstice Child
Template: freeframe-solstice
*/

Templates, parts, patterns and theme.json in the child theme override the parent's files of the same name. To change the festival data, copy assets/js/data.js to the child theme and load it instead of the parent's:

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

The parent's stylesheet only contains the classes the parent uses: if your child theme's patterns use 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#

  • An artist doesn't appear in the line-up or timetable. Check its day, stage and genre ids exist in SOLSTICE.days, stages and genres, and that start/end are "HH:MM".
  • A set opens on the wrong day or the countdown is an hour out. Times are festival-local; check utcOffset in data.js and the offset in data-countdown. Test with ?now=.
  • Prices in the blocks are different from the shop. The script replaces every data-price-of price with the one in SOLSTICE.tickets; the typed prices are only a no-JavaScript fallback.
  • "Continue to details" stays disabled. The basket is empty or breaks a rule (a campervan pitch or bell tent without a camping ticket); the summary says which.
  • The pages look unstyled or new classes do nothing. The stylesheet only contains classes it found when it was built. Run pnpm build after editing patterns, then reload.
  • "This block contains unexpected or invalid content". A pattern or page was edited by hand with markup that doesn't match the block. Click Attempt recovery, or re-insert the section from Patterns > Solstice.
  • The header menu shows other pages, or an old menu. A Navigation block without a chosen menu shows the most recent one. Select it and choose "Main menu" (or yours) in the block settings, in both the desktop and the mobile Navigation block.
  • Solstice 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 > Solstice pages.
  • The forms don't send anything. They are demo forms; see Forms.
  • A page shows the title twice or no title. Pages use a template without a title, because the page patterns start with their own heading. For a plain text page (privacy policy, terms of entry), choose the Page with title template in the page's settings.

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 artwork (hero sun, hills, bunting, maps, the generated artist art and the logo) is included under the same license. DM Sans, Bricolage Grotesque and Shrikhand are licensed under the SIL Open Font License 1.1 (assets/fonts/*/OFL.txt).

Search documentation

Search the guides and every template's documentation