Freeframe

Documentation

Gather

v1.0.1Updated Oct 16, 2026A warm, modern church website with a live service countdown, sermon player, events calendar and visit planner.

Overview#

Gather is a site for a church, built as a WordPress block theme. The fictional brand is Riverbend Church, a non-denominational church on the bend of the Kansas River in Lawrence, Kansas, with two Sunday gatherings, small groups, a food pantry and a youth night. The theme has a home page with this Sunday's times, a countdown to the next service, the latest sermon with an audio player, the next events, the ministries and a newsletter sign-up; a plan-your-visit page with what to expect, service times, kids' check-in, accessibility, a parking map, an FAQ and a visit form with a live summary; a sermon archive filtered by series, speaker and book; sermon pages with notes, a key verse and discussion questions; an events calendar with a month grid, a list, a detail dialog and .ics downloads; a small-group finder and the ministries; a giving form with a live total; an about page with the staff; a contact page with a prayer request form; and a 404 page. It is version 1.0.0 (see template.json and style.css), works with WordPress 7.1 and PHP 7.4 or newer, and is the WordPress version of the HTML template of the same name. See it live at freeframe.dev/templates/gather.

The pages (headings, copy, cards, buttons, lists, FAQs) are core WordPress blocks arranged in block patterns, so you edit them in the block editor without touching code. The parts that change with the date or come from data work as in the HTML version: the service times, sermons, events, groups, giving settings and staff are lists in assets/js/main.js, and the script draws the countdown, the players, the sermon archive and pages, the calendar, the group finder and the staff grid from them. There are no photos: every illustration, portrait and series cover is SVG. Colours and fonts are design tokens in theme.json: change them in Appearance > Editor > Styles and the whole site follows.

Quick start#

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

  1. Download gather.zip from freeframe.dev/templates/gather (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 Gather.
  3. Go to Appearance > Gather setup and click Create the demo pages. This creates the Home, Plan your visit, Sermons, Sermon, Events, Groups & ministries, Give, About us and Contact & prayer 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.
  4. In Settings > General, the demo uses "Riverbend" as the Site Title and "Church · Lawrence" as the Tagline: the header and footer show both next to the logo. Set your own.
  5. 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:

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 > Gather pages) made of section patterns. Every section is also offered on its own in the block inserter under Patterns > Gather.

Page Slug Page pattern Sections (pattern files in patterns/)
Home / page-home.php home-hero, this-sunday (times, countdown, address), new-here, latest-message (cover and player), home-events, ministries, open-door (verse and numbers), newsletter, event-dialog
Plan your visit /visit/ page-visit.php visit-hero, visit-nav (links to the sections), visit-expect, visit-times, visit-kids, visit-access, visit-parking (map), visit-faq, visit-plan (form and summary)
Sermons /sermons/ page-sermons.php sermons-hero, sermons-live (countdown and latest player), sermons-series (series cards), sermons-archive (filters, search, results)
Sermon /sermon/?id=room-at-the-table page-sermon.php sermon-hero (breadcrumb, title, cover, player), sermon-body (key verse, notes, questions, speaker, series), sermon-related
Events /events/ page-events.php events-hero, events-calendar (filters, month grid, selected day, list), events-host, event-dialog
Groups & ministries /groups/ page-groups.php groups-hero, group-finder, ministries-detail
Give /give/ page-give.php give-hero, give-form (form and summary), give-budget (budget bar and funds), give-faq
About us /about/ page-about.php about-hero, about-story (timeline), beliefs, how-led, staff
Contact & prayer /contact/ page-contact.php contact-hero, contact-form (office, pastor on call, form), prayer
404 any missing URL templates/404.html 404

The script links to the home, sermons, sermon and contact pages by their slugs (/, sermons, sermon, contact): if you rename one, change its URL in freeframe_gather_page_urls() in inc/setup.php too. It recognises each page by the widget it contains (the sermon list, the calendar, the visit form...), so the sections keep working wherever you place them. 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-gather/
  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, 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 Warm button style
  inc/site-setup.php        Appearance > Gather 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         service times, sermons, events, groups, giving and staff data, and
                            everything that uses them
  assets/icons/             SVG icons for the Icon block
  assets/img/               logo, favicon, social image, ministry icons, table illustration
  assets/fonts/             Nunito Sans and Urbanist, 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-gather.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:

Colour Default Used for
Paper #fbf6ee the page background
Shell #fffdf9 cards and form fields
Sand #f4eadb soft panels, the latest-message band
Line #e5d8c5 hairlines and borders
Ink #2b1b36 text, dark bands, the footer
Muted #5f5468 secondary text
Plum, Plum dark #5b2a6e, #431d52 the brand: primary buttons, links, eyebrows
Lilac #efe5f5 plum tints, the current menu item
Apricot, Apricot light, Apricot dark #f6a96b, #fde3cc, #9a4716 warm buttons and accents (Apricot is for backgrounds; Apricot dark is its text colour)
Sage, Sage light, Sage dark #c9dcc0, #e6efe0, #33603b panels, success messages
River, River light, River dark #bcd9e8, #e3f0f6, #1f5577 kids' panels and accents
Rose #b3364a errors and "Live now"
White, Black for your own blocks

The theme's stylesheet reads these colours from the palette (bg-plum, text-muted...), so changing Plum in Styles recolours every button and link at once. A few colours are written directly into SVG: the illustrations (hero, welcome tent, parking map, 404 signpost), the portraits (PEOPLE[].avatar) and series covers (seriesArt()) in assets/js/main.js, the ministry icons in assets/img/, the logo (assets/img/logo.svg) and the favicon.

Typography#

Two variable fonts are bundled in assets/fonts/ and declared in theme.json (settings.typography.fontFamilies), so no request goes to Google: Urbanist (500 to 900; preset display) for headings, numbers and buttons' big text, and Nunito Sans (400 to 800, italic 400 to 600; preset sans) for text. To use other fonts, install them in Styles > Typography > Fonts (the Font Library) and select them, or replace the fontFace entries in theme.json. The utility classes read the presets (--wp--preset--font-family--display, --sans), so keep those slugs. The text inside the illustrations and the player's "15" names the fonts directly (font-family="Urbanist, sans-serif", font-family="Nunito Sans, sans-serif").

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 > Gather.

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, visit-hero, 404 the illustrations SVG with a <title> (the hero's river shimmers)
this-sunday, sermons-live <div data-countdown> (data-dark on the dark one) see Service status and countdown
latest-message, sermons-live #latest-art, #latest-sermon, #latest-player the latest sermon's cover and player
home-events #home-events the next four events
event-dialog <dialog id="event-dialog"> see Events calendar
sermons-series, sermons-archive the series cards, filters, count, results and empty state see Sermon archive
sermon-hero, sermon-body, sermon-related the breadcrumb, cover, player, verse, notes, questions, speaker, series list and related sermons see Sermon page
events-calendar the filters, view switch, count, month grid and list see Events calendar
group-finder, ministries-detail the filters, results and the leaders' portraits see Group finder
visit-parking, give-budget the map and the budget bar with their legends illustrations with text alternatives
newsletter, visit-plan, give-form, contact-form, prayer the forms and their summaries see Forms
staff #staff-grid the staff cards, from PEOPLE and STAFF
header, footer the service status, the menu button and the footer wave 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 attribute data-<name>="<value>" when the page loads (blocks can't store data attributes): ffd-this-sunday on the home page's "This Sunday" heading marks it to be filled with the date of the next Sunday.
  • reveal fades a block in as it scrolls into view.
  • link-card on a group makes the whole group clickable: the link in its heading stretches over the group (the "New here?" steps, the ministry cards, the logo).
  • eyebrow is the small plum heading with an apricot dot, h-display and h-section the Urbanist headline styles, card and card-hover the white card and its lift on hover, link the plum link with an apricot underline, faq the FAQ answers' style.
  • ff-blob ff-blob-home (and -visit, -groups, -about), ff-wave-top, ff-waves ff-waves-quote (and -live) and ff-circle draw the soft shapes behind sections, and ff-swash the underline under "saved for you."

Service times, sermons, events and groups

The data is at the top of assets/js/main.js, as in the HTML version:

Constant What it holds
CHURCH name, timeZone ('America/Chicago'), timeZoneLabel, address for calendar files, liveUrl, uidDomain
SERVICES weekly services: day (0 = Sunday), start ('09:00', church time), minutes, name
KEEP_DEMO_CURRENT, DEMO_ANCHOR see Demo dates
PEOPLE, STAFF staff with name, role, email, bio and portrait settings; STAFF orders the about page grid
SERIES, BOOKS, SERMONS sermon series (with their cover motif art), books and sermons
EVENT_CATS, EVENTS event categories (label and colours) and events
DAYS, AREAS, STAGES, GROUPS the group finder's options and the groups
GIVING amount chips, default amount, minimum and maximum, card fee, funds
VISIT how many Sundays to offer, party limits and the kids' rooms by age

Add a sermon to SERMONS (the archive sorts by date). series, speaker and book must be keys of SERIES, PEOPLE and BOOKS; duration is in seconds; leave audio empty for the simulated player or set it to an audio file's URL:

js
{
  id: 'the-good-neighbour', title: 'The Good Neighbour', series: 'open-table', part: 6, speaker: 'dana',
  date: '2026-10-18', book: 'luke', passage: 'Luke 10:25–37', duration: 2280, audio: '',
  summary: 'Who is my neighbour?',
  verse: { text: 'Go, and do thou likewise.', ref: 'Luke 10:37 (KJV)' },
  notes: [['A question to win an argument', 'The lawyer wants a boundary.']],
  questions: ['Who is the hardest neighbour for you to love?']
}

Add an event to EVENTS. cat is a key of EVENT_CATS; start and end are 24-hour church times; weekly repeats it every week up to and including that date:

js
{ id: 'choir-night', title: 'Choir Night', cat: 'worship', date: '2026-11-05', weekly: '2026-12-17',
  start: '19:00', end: '20:30', place: 'Main auditorium', summary: 'Open rehearsal for anyone who sings.' }

Add a group to GROUPS; day, area and stage must be keys of DAYS, AREAS and STAGES, and spots: 0 shows "Full: join the waitlist":

js
{ id: 'lunch-break', name: 'Lunch Break', day: 'wed', time: '12:15', area: 'downtown', stage: 'everyone',
  host: 'Ana Ruiz', where: 'Riverbend, Room 210', spots: 6, childcare: false, blurb: 'A 45-minute study over lunch.' }

Edit assets/js/main.js in a child theme (see Child themes), or your changes are overwritten when you update the theme. Sermon, event, group and staff texts are data, not translatable strings; the labels around them (categories, days, areas, funds, buttons, messages) are translatable.

Demo dates

The demo is written for the week of DEMO_ANCHOR ('2026-10-11'). While KEEP_DEMO_CURRENT is true, every sermon and event date moves forward by whole weeks so that the latest sermon is always last Sunday and the events are always upcoming. Weekdays never change. Set KEEP_DEMO_CURRENT = false once you enter real dates, or your dates will be shifted too.

Images and artwork#

There are no photos. All artwork is SVG:

Artwork Where
Hero landscape (church, river, hills), welcome tent, 404 signpost Custom HTML blocks in home-hero, visit-hero and 404
Small-group table assets/img/illustration-eight-people-sitting-around-a-table-sharing-a-meal.svg (Image block in groups-hero)
Ministry icons assets/img/ministry-kids.svg, -youth, -care, -music (Image blocks)
Parking map Custom HTML in visit-parking, 640×420, with a <title> and <desc>
Staff portraits avatar() in main.js, drawn from each person's avatar settings
Series covers seriesArt() in main.js, 320×200, varied by the sermon's part
Logo assets/img/logo.svg in the header and footer, and assets/img/favicon.svg

A portrait's avatar object takes skin, hair, style (short, long, bun, curly, coily, waves or bald), bg, shirt, and optional glasses and beard. Elements with data-avatar="dana" are filled with that portrait (add data-decorative to hide it from screen readers). In the editor the covers and portraits are empty boxes: the script draws them on the site.

To use a photo instead of an illustration, replace its Custom HTML block with an Image block (the hero frame is about 10:9); for portraits (1:1) and series covers (16:10), return an <img> from avatar() or seriesArt(). Give every image meaningful alt text.

The logo is assets/img/logo.svg in an Image block next to the Site Title and Site Tagline blocks in the header and footer; the words come from Settings > General. Icons are Icon blocks: select one and pick another from the Gather 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.

The header and footer are template parts (parts/header.html, parts/footer.html) showing the header and footer patterns. Edit them in Appearance > Editor > Patterns > Header / Footer. The header pattern starts with the dark status bar (the service status and "New here? Start here").

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": Plan a visit, Sermons, Events, Groups, About, Contact, Give). Its last link (Give) is hidden in the desktop header, which has the Give button instead, as in the HTML version; keep Give last, or remove the .site-nav … :last-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 in the block settings, for both blocks. The Give button is marked as the current page on the Give page.

The footer's Visit and Connect menus are List blocks: click a link to change it, press Enter to add one.

Features#

Service status and countdown#

serviceState(), updateStatus() and initCountdowns() in section 7 of assets/js/main.js. The bar above the header ([data-service-status]) shows "Next gathering: Sunday at 9:00 am", or "Live now" with a "Watch" link while a service is running. The [data-countdown] boxes (home and sermons pages) show days, hours, minutes and seconds to the next service and switch to a "Live now" card with a "Watch live" button linking to CHURCH.liveUrl (the sermons page's #live section). Elements with data-this-sunday show the date of the next Sunday with a service still to come.

Everything is computed from SERVICES in CHURCH.timeZone, so a visitor in another time zone sees the right countdown for your church. Add a service by adding to SERVICES:

js
{ day: 3, start: '18:30', minutes: 60, name: 'Midweek Worship' }

To preview the live state at any time, add ?live=1 to a page's URL, for example /?live=1. The ticking numbers are hidden from screen readers; a visually hidden sentence ("Starts in 5 days, 18 hours and 53 minutes.") gives the same information without announcing every second.

Audio player#

playerMarkup() and initPlayer() (section 8). It appears on the sermon page, on the home page (latest sermon) and in the "Watch live" panel on the sermons page.

  • With a real file: set the sermon's audio to a URL (from your media library or podcast host). The player then drives a real <audio> element: its duration comes from the file, and speed, seeking and progress use the media element.
  • Without a file (audio: '', the demo): playback is simulated over the sermon's duration with a timer, so every control works and a note under the player says so.

Controls: play/pause, a slider for scrubbing (5-second steps), back and forward 15 seconds, and a speed button cycling through SPEEDS (1×, 1.25×, 1.5×, 1.75×, 2×, 0.75×). Only one player plays at a time. Keyboard, while focus is inside the player: Space (when not on a button) or K play/pause, J and L back and forward 15 seconds, arrow keys on the slider move 5 seconds, Home/End jump to the start or end. The slider has an aria-valuetext such as "12 minutes 5 seconds of 38 minutes 42 seconds", the play button's label changes between "Play" and "Pause", and speed changes are announced through a polite live region. The listening position is saved (see Saved data); returning to a sermon shows "Resuming at 12:04" and cards in the archive show "Resume at 12:04".

Sermon archive#

initSermons() (section 9) fills the series, speaker and book selects from the data and filters on every change. Search matches every word against the title, passage, summary, series and speaker. The filters are kept in the URL, so links can be shared: /sermons/?series=open-table&speaker=dana&book=luke&q=table (names and values are case-insensitive; unknown values are ignored). Series cards toggle the series filter. The result count is a live region; with no results an empty state offers to clear the filters.

Sermon page#

initSermon() (section 10). /sermon/?id=<id> shows one sermon; an unknown or missing id shows the latest sermon (with a notice when the id is unknown). The browser tab title and the meta description (if your SEO plugin prints one) are updated from the sermon, and the title, summary, date, passage, length, cover, player, key verse, notes, questions, speaker card, series list and related sermons are filled in; the blocks contain Room at the Table as default text. "Copy questions" copies the title, passage and numbered questions to the clipboard (it needs https or localhost). Related sermons are the rest of the series, then other sermons by the same speaker.

Events calendar#

initEvents() (section 11) shows every occurrence of EVENTS (weekly events are expanded). The month view is a table with one button per day; choosing a day shows its events in the "Selected day" panel, with the next three events after it. On wide screens each day lists up to two events and a "+n more" button. The list view shows weekly events once in an "Every week" box, then upcoming events by month. Category pills filter both views, and the state is kept in the URL: /events/?view=list&cat=kids,serve&month=2026-11. Opening an event adds &event=<id>-<date>, so a link opens straight to that event; the home page's event cards open the same dialog.

The detail is a native <dialog> (the event-dialog pattern: focus is trapped, Esc closes it and focus returns to the button that opened it). "Add to calendar" downloads an .ics file built by buildIcs(), with start and end times converted from the church's time zone to UTC, so it imports correctly into Google Calendar, Apple Calendar and Outlook wherever the visitor is.

Group finder#

initGroups() (section 12) renders filter pills for day, area and season of life from DAYS, AREAS and STAGES, each with a live count; options that would give no results are disabled. "Childcare provided" is a checkbox. The state is in the URL: /groups/?day=tue,thu&area=west&stage=families&childcare=1. "Ask to join" links to /contact/?topic=groups&group=<id>, which pre-fills the contact form.

Plan your visit#

initVisit() (section 13) offers the next VISIT.sundays Sundays (computed in the church's time zone; today is skipped once the last Sunday service has started) and one radio card per Sunday service. Adults and children use steppers; each child gets an age select, and the summary beside the form shows which room they go to from VISIT.rooms. Children in the last room (youth) count as an extra seat.

Giving#

initGive() (section 14) builds the amount chips from GIVING.amounts and the fund select from GIVING.funds. Typing a custom amount clears the chips. "Cover the card fee" adds what is needed so that, after the fee in GIVING.fee (2.2% + $0.30 by default), the church receives the whole gift; the summary shows the total and what the church receives either way. /give/?fund=pantry pre-selects a fund. No payment is taken: see Forms.

Scroll reveal and back to top#

Blocks with the reveal class fade in as they scroll into view; everything above the current block is revealed too, so jumping to an anchor never leaves gaps. With reduced motion, nothing is hidden or animated (the "Live now" pulse and the river shimmer stop too). The back-to-top button is created by the last block of main.js; delete that block and the .back-to-top styles in src/theme.css, then run pnpm build, to remove it.

Block styles#

In the block settings' Styles panel:

Block Styles
Button Fill (default, the plum button), Outline (white with a thin ring), Warm (apricot)

The classes is-small, is-px-7, is-px-8, is-py-3-5, is-py-4 and is-text-base on a Button block make it smaller, wider or taller (the hero buttons use them).

FAQ answers (plan your visit and give pages) are Details blocks with the class faq: add one by duplicating an answer in List View; the round plus and minus button is drawn by CSS.

Forms#

All forms are validated by setupValidation() in section 15 of main.js: errors appear on blur and submit, the first invalid field is focused, each error is linked with aria-describedby, and a status line says "Please fix the highlighted fields." Rules come from attributes in the forms' Custom HTML:

Attribute Rule
required Must not be empty (checkboxes must be ticked, selects must have a value)
type="email", type="tel" Must look like an email address / phone number
minlength Minimum length
data-label Name used in the error message
data-novalidate Field is skipped (used by filters)
Form Where Behaviour
Newsletter (data-form="newsletter") Home (newsletter) Replaced with a thank-you message
Plan your visit (#visit-form) Plan your visit (visit-plan, #plan) Validates contact details and each child's age; confirms the Sunday, time and seats
Giving (#give-form) Give (give-form) Validates the amount (minimum and maximum from GIVING), name and email; explains nothing was charged
Contact (#contact-form) Contact (contact-form) Pre-filled from ?topic= and ?group=; thank-you message
Prayer (#prayer-form) Contact (prayer, #prayer) Request required; email becomes required when "I'd like a pastor to get in touch" is ticked; the message reflects "Keep this private"

Demo forms don't send anything, 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 its callback in assets/js/main.js (in a child theme), for example to Formspree:

js
setupValidation(contact, () => {
  fetch('https://formspree.io/f/your-id', {
    method: 'POST', body: new FormData(contact), headers: { Accept: 'application/json' }
  }).then((res) => successSwap(contact, res.ok ? '<p>Thanks, we got your message.</p>' : '<p>Something went wrong. Please email us.</p>'));
});

Every field has a name, so FormData carries all the values; keep a name on any field you add. Prayer requests can be sensitive: send them to a service and inbox only your pastoral team can read, and honour the private field.

Connecting a payment provider#

The giving form never collects card details. To take real gifts, replace the successSwap(...) call in the setupValidation(form, ...) callback inside initGive() with a request to your server, which creates a checkout session with your provider (Stripe Checkout, PayPal, or a church platform such as Tithe.ly or Planning Center Giving) and returns its URL:

js
fetch('/wp-json/your-plugin/v1/gift', { method: 'POST', body: new FormData(form) })
  .then((res) => res.json())
  .then((data) => { window.location.href = data.url; });

Recurring gifts (frequency is weekly or monthly) map to subscriptions in most providers. Many giving plugins and church platforms also offer a block or embed you can use instead of this form.

Saved data (localStorage)#

Key Shape Used for
riverbend-sermon-progress { "room-at-the-table": 724 } (seconds) Resume position for each sermon

readProgress() keeps only ids that exist in SERMONS and positions between 5 seconds and 5 seconds before the end; anything else is dropped. A position is removed when a sermon finishes. If storage is blocked, the player works without saving. To reset, run localStorage.removeItem('riverbend-sermon-progress') in the browser console, or change PROGRESS_KEY to stop using it.

SEO and sharing#

Page titles come from WordPress: the page title, a dash and the site name (for example "Sermons — Riverbend"); the home page shows the site name and tagline (Settings > General). The sermon page sets its title from the sermon ("Room at the Table — Open Table — Riverbend") once the script runs. 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. Sermons are drawn by JavaScript on one page; if search visibility for each sermon matters, give each sermon a page or post of its own. Consider adding Church structured data (a schema.org type) with your address and service times through your SEO plugin.

Accessibility#

Built in: a skip link to the main content (added by WordPress, styled by the theme), landmarks (header, labelled navigation, main, footer), one h1 per page, labelled sections, a breadcrumb with aria-current on sermon pages, visible focus rings, the menu button with aria-expanded and a label that changes with its state (Escape closes the menu, and it closes when the window grows to desktop width), a fully keyboard-operable audio player with a native range slider and spoken time, a calendar table with a caption, column headers and full-date day labels, a modal <dialog> for event details, live regions for counts and summaries, FAQ answers in <details>, real checkboxes and radios behind the pills and cards, text alternatives for the illustrations (the map also has a <desc>), form errors announced through aria-describedby and aria-invalid, 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 don't use Apricot for text (use Apricot dark).

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/:

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, 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-gather, 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-gather. All text in patterns, templates and PHP goes through WordPress's translation functions; the script's strings (the countdown, player, archive, calendar, group finder, summaries and form messages) are translated in PHP (freeframe_gather_script_strings() in inc/setup.php), and dates, times and amounts are formatted for the site's language. Sermons, events, groups and staff are data in assets/js/main.js. Create a translation with Loco Translate, or with WP-CLI and Poedit:

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

Save freeframe-gather-de_DE.mo (for example) in wp-content/languages/themes/ or in the theme's languages/ folder. Page content created by Gather 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 Gather is updated (your sermons and events in particular). Create wp-content/themes/gather-child/style.css:

css
/*
Theme Name: Gather Child
Template: freeframe-gather
*/

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-gather) and enqueue yours with the same dependencies in the child's functions.php. 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 countdown is an hour off. Check CHURCH.timeZone is your church's IANA time zone (for example America/New_York), not a city name or an offset.
  • My sermons or events have the wrong dates. KEEP_DEMO_CURRENT is still true, so dates are moved forward. Set it to false.
  • 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 > Gather.
  • Sermon cards or "Watch live" link to a page that doesn't exist. The script links to /sermon/, /sermons/ and /contact/. If you renamed those pages, update freeframe_gather_page_urls() in inc/setup.php.
  • A new sermon or group doesn't show under a filter. Its series, speaker, book, day, area or stage must be a key of the matching constant.
  • Gather 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 > Gather pages.
  • The forms don't send anything. They are demo forms; see Forms.

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 illustrations, portraits, series covers, logo and social image are included and covered by the same license. Nunito Sans and Urbanist are under the SIL Open Font License 1.1 (OFL.txt in each font folder). Scripture quotations in the demo are from the King James Version, which is in the public domain.

Search documentation

Search the guides and every template's documentation