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.
- Download
gather.zipfrom freeframe.dev/templates/gather (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 Gather.
- 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.
- 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.
- 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 > 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.txtCustomising#
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 attributedata-<name>="<value>"when the page loads (blocks can't store data attributes):ffd-this-sundayon the home page's "This Sunday" heading marks it to be filled with the date of the next Sunday.revealfades a block in as it scrolls into view.link-cardon 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).eyebrowis the small plum heading with an apricot dot,h-displayandh-sectionthe Urbanist headline styles,cardandcard-hoverthe white card and its lift on hover,linkthe plum link with an apricot underline,faqthe FAQ answers' style.ff-blob ff-blob-home(and-visit,-groups,-about),ff-wave-top,ff-waves ff-waves-quote(and-live) andff-circledraw the soft shapes behind sections, andff-swashthe 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:
{
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:
{ 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":
{ 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.
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 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:
{ 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
audioto 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'sdurationwith 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:
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:
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/:
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:
wp i18n make-pot wp-content/themes/freeframe-gather wp-content/themes/freeframe-gather/languages/freeframe-gather.potSave 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:
/*
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.timeZoneis your church's IANA time zone (for exampleAmerica/New_York), not a city name or an offset. - My sermons or events have the wrong dates.
KEEP_DEMO_CURRENTis stilltrue, so dates are moved forward. Set it tofalse. - 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 > 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, updatefreeframe_gather_page_urls()ininc/setup.php. - A new sermon or group doesn't show under a filter. Its
series,speaker,book,day,areaorstagemust 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.