Overview#
Gather is a website for a local church, built for Riverbend Church, a fictional non-denominational church in Lawrence, Kansas. It has a home page with this Sunday's times and a countdown to the next service, a plan-your-visit page, a sermon archive and sermon pages with an audio player, an events calendar with calendar downloads, a small-group finder and ministries, a giving page, an about page with the staff team, contact and prayer requests, and a 404 page. This is the Astro version: Astro 7, TypeScript and Tailwind CSS v4, with no UI framework; the interactive parts are one plain TypeScript module. It builds to static HTML, CSS and JavaScript. This guide covers version 1.0.0. See it live at freeframe.dev/templates/gather.
Quick start#
Requirements: Node.js 24 or newer and pnpm (npm works too).
pnpm install
pnpm dev # http://localhost:4321With npm: npm install, then npm run dev.
| Command | What it does |
|---|---|
pnpm dev |
Development server at http://localhost:4321 with hot reload |
pnpm build |
Static site in dist/ |
pnpm preview |
Serve dist/ locally after a build |
pnpm check |
astro check: type-checks .astro files and TypeScript |
pnpm typecheck |
Generate Astro's types, then type-check with tsc (TypeScript 7) |
pnpm lint |
ESLint (TypeScript and Astro rules) |
pnpm build:demo |
Build for /demos/astro/gather into demo/ (only used for the demo on freeframe.dev) |
Pages#
| Route (file) | Purpose | Notable sections and features |
|---|---|---|
/ (src/pages/index.astro) |
Home | Hero with river illustration, "This Sunday" card with times and the next-service countdown, three-step "I'm new" path, latest sermon with player, next four events, ministries, scripture and figures band, newsletter sign-up |
/visit (visit.astro) |
Plan your visit | What to expect, service times, kids' check-in steps and rooms, accessibility, SVG parking map, FAQ accordion, plan-your-visit form with live summary |
/sermons (sermons.astro) |
Sermon archive | "Watch live" panel with countdown and the latest message, series cards, search and series/speaker/book filters with URL state, sermon cards |
/sermon?id=room-at-the-table (sermon.astro) |
One sermon | Series cover, audio player, key verse, notes, discussion questions with copy button, speaker card, the rest of the series, related sermons |
/events (events.astro) |
Events | Category filter, month calendar and list views, selected-day agenda, event dialog with "Add to calendar" (.ics) |
/groups (groups.astro) |
Groups and ministries | Small-group finder (day, area, season of life, childcare), ministries for kids, youth, care and music (#kids, #youth, #care, #music) |
/give (give.astro) |
Give | Frequency, amount chips and custom amount, fund, cover-the-fee option, live total, "connect your payment provider" note, budget bar, funds, FAQ |
/about (about.astro) |
About | Story timeline, beliefs (#beliefs), leadership, staff grid with SVG portraits |
/contact (contact.astro) |
Contact and prayer | Office details, urgent pastoral contact, contact form, prayer request form (#prayer) |
404.html (404.astro) |
Not found | Signpost illustration and links back; not indexed by search engines |
Project structure#
gather/
astro.config.mjs site URL, base path, static output as page.html, Tailwind
package.json scripts and dependencies
tsconfig.json Astro's strict TypeScript preset
eslint.config.mjs ESLint flat config (typescript-eslint + eslint-plugin-astro)
public/img/ favicon.svg, og-image.svg (source) and og-image.png
src/
pages/*.astro one file per page (index.astro → /, visit.astro → /visit …, 404.astro)
layouts/Layout.astro <html>, <head> metadata, header, <main>, footer and the script import
components/
SiteHeader.astro service status bar, logo, main navigation, Give button and mobile menu
SiteFooter.astro footer with link columns, address and Give button
Logo.astro the Riverbend mark (sun, hill and river)
data/
church.ts CHURCH settings, SERVICES schedule, KEEP_DEMO_CURRENT, DEMO_ANCHOR
people.ts PEOPLE (staff and speakers with avatar settings) and STAFF order
sermons.ts SERIES, BOOKS, SERMONS and PROGRESS_KEY
events.ts EVENT_CATS and EVENTS
groups.ts DAYS, AREAS, STAGES and GROUPS
ministries.ts MINISTRIES (home page cards and groups page panels)
settings.ts GIVING and VISIT settings
nav.ts header, mobile menu and footer links
lib/
site.ts site name, theme colour, address and the url() helper
art.ts avatar(), seriesArt(), ICON and escapeHtml()
scripts/
main.ts every interactive feature (countdown, player, filters, calendar, forms …)
back-to-top.ts the back-to-top button
styles/global.css Tailwind source: brand tokens and component classes
README.md, DOCS.md, CHANGELOG.mdsrc/scripts/main.ts is split into numbered sections (listed at the top of the file): 1 dates and the demo
shift, 2 helpers, 3 header, menu, reveal and countdown, 4 audio player, 5 sermon archive, 6 sermon page,
7 events calendar, 8 groups finder, 9 plan your visit, 10 giving form, 11 forms, 12 boot. Each page's
features start from <body data-page> (set by the page prop of Layout).
Content that depends on today's date (sermon dates, events, the countdown, upcoming Sundays) or on the URL
(filters, ?id=) is rendered in the browser by main.ts, so a static build never goes out of date. Content
that only depends on the data (the staff grid, ministries, navigation) is rendered at build time.
Customising#
Brand and colours#
The design tokens are in the @theme block of src/styles/global.css. Every utility such as bg-plum or
text-muted comes from them.
| Token | Default | Used for |
|---|---|---|
--color-paper |
#fbf6ee |
Page background |
--color-shell |
#fffdf9 |
Cards and form fields |
--color-sand |
#f4eadb |
Soft panels, filter bars |
--color-line |
#e5d8c5 |
Hairlines and card rings |
--color-ink |
#2b1b36 |
Text, the footer, dark bands and the audio player |
--color-muted |
#5f5468 |
Secondary text |
--color-plum / --color-plum-dark |
#5b2a6e / #431d52 |
Brand colour, primary buttons, links |
--color-lilac |
#efe5f5 |
Selected states, worship events |
--color-apricot / -light / -dark |
#f6a96b / #fde3cc / #9a4716 |
Warm accent (backgrounds), tint, accent text |
--color-sage / -light / -dark |
#c9dcc0 / #e6efe0 / #33603b |
Hills, care ministry, serve events, success |
--color-river / -light / -dark |
#bcd9e8 / #e3f0f6 / #1f5577 |
River, kids' ministry and events |
--color-rose |
#b3364a |
Errors, "Live now", classes events |
To change the brand colour, edit the tokens (Astro rebuilds the CSS on save in pnpm dev):
@theme {
--color-plum: #1f5c7a;
--color-plum-dark: #164459;
}Use apricot only as a background: for text on light backgrounds use apricot-dark, which passes
WCAG AA. The SVG artwork (hero, map, avatars, series covers, logo) uses literal hex values; search
src/ for #5b2a6e and #f6a96b to recolour it. Also update themeColor in src/lib/site.ts.
Component classes in global.css: .btn-primary, .btn-warm, .btn-outline, .btn-light,
.btn-sm, .card, .card-hover, .eyebrow, .h-display, .h-section, .tag, .chip-dot, .link,
.field, .field-label, .field-error, .option and .pill (radio and checkbox cards that highlight
when checked), .check, .faq (the accordion) and .scrub (the player's slider).
Typography#
Two Google Fonts are loaded by a <link> in src/layouts/Layout.astro: Urbanist for headings
(font-display) and Nunito Sans for body text (font-sans). To swap them, change the family= parts of
that link and the names in global.css:
--font-sans: "Nunito Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--font-display: "Urbanist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;Content#
Page copy is in the .astro files in src/pages/. Everything that is listed, filtered or calculated is
typed data in src/data/:
| Export | File | What it holds |
|---|---|---|
CHURCH |
church.ts |
Name, timeZone (IANA name, e.g. America/Chicago), timeZoneLabel, full address for calendar files, liveUrl (a site path), uidDomain |
SERVICES |
church.ts |
The weekly schedule: day (0 = Sunday), start ('09:00'), minutes, name |
KEEP_DEMO_CURRENT, DEMO_ANCHOR |
church.ts |
Moves demo dates forward (see Demo dates) |
PEOPLE, STAFF |
people.ts |
Staff and speakers with role, email, bio and avatar; STAFF is the order on the about page |
SERIES, BOOKS, SERMONS |
sermons.ts |
Sermon series (art: table, sun, wheat or wave), labels for the book filter, every sermon |
EVENT_CATS, EVENTS |
events.ts |
Event categories with their colour classes, and events |
DAYS, AREAS, STAGES, GROUPS |
groups.ts |
Small-group filter labels and groups |
MINISTRIES |
ministries.ts |
The four ministries: home page card text and the groups page panel |
GIVING, VISIT |
settings.ts |
Amount chips, default amount, min and max, card fee, funds; number of Sundays offered, maximum party size, kids' rooms by age |
mainNav, mobileNav, footerNav |
nav.ts |
Header, mobile menu and footer links |
site |
src/lib/site.ts |
Name, theme colour, street, city, phone and email used in the header and footer |
The types (Sermon, ChurchEvent, Group, Person, Ministry …) are exported next to the data, so the
editor and pnpm check tell you when a field is missing or misspelled.
Add a sermon by adding an object to SERMONS (order doesn't matter; the archive sorts by date):
{
id: 'a-new-sermon', title: 'A New Sermon', series: 'open-table', part: 6, speaker: 'dana',
date: '2026-10-18', book: 'luke', passage: 'Luke 10:38–42', duration: 2100, audio: '',
summary: 'One or two sentences for cards and the page description.',
verse: { text: 'Quoted text.', ref: 'Luke 10:41 (KJV)' },
notes: [['Heading', 'One short paragraph.'], ['Heading', 'Paragraph.']],
questions: ['First question?', 'Second question?']
},speaker is a key of PEOPLE, series a key of SERIES, book a key of BOOKS, and duration is in
seconds. The demo quotes the King James Version, which is in the public domain; check the licence of any
other translation before quoting it.
Add an event to EVENTS. Times are 24-hour, in the church's time zone; add weekly to repeat it every
week until 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, 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.' },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 (in the browser, from today's date in the church's
time zone) so that the latest sermon is always last Sunday and events are always upcoming. Weekdays never
change. Set KEEP_DEMO_CURRENT = false in src/data/church.ts 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) | Inline in src/pages/index.astro, 600×540 |
| Welcome tent, small-group table, 404 signpost | Inline in visit.astro, groups.astro, 404.astro |
| Parking map | Inline in visit.astro, 640×420, with a <title> and <desc> |
| Ministry icons | icon in src/data/ministries.ts (64×64) |
| Staff portraits | avatar() in src/lib/art.ts, drawn from each person's avatar settings |
| Series covers | seriesArt() in src/lib/art.ts, 320×200, varied by the sermon's part |
| Logo | src/components/Logo.astro, and public/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. In a page, render a portrait with
<Fragment set:html={avatar("dana", "Portrait of Dana Okafor")} />; pass "" as the label for a
decorative one.
To use photos instead, put them in public/img/ and replace the generated markup with
<img src={url("/img/dana.jpg")} alt="…"> (in .astro files) or an <img> string (in main.ts):
portraits are square (1:1), series covers 16:10, and the hero frame about 10:9. Give every image meaningful
alt text.
The social image is public/img/og-image.png (1200×630), rendered from og-image.svg. Edit the SVG, then
export it to PNG at 1200×630 with any vector editor or a browser screenshot.
Navigation, header and footer#
The header and footer are components (src/components/SiteHeader.astro, SiteFooter.astro) used by
Layout.astro, and their links come from src/data/nav.ts. To add a page, create src/pages/<name>.astro
using Layout, then add it to mainNav (desktop and mobile) or footerNav:
export const mainNav: NavLink[] = [
{ key: "visit", href: "/visit", label: "Plan a visit" },
{ key: "stories", href: "/stories", label: "Stories" },
// …
];A page passes the same key as current to highlight its link with aria-current="page":
<Layout title="Stories — Riverbend Church" description="…" page="stories" current="stories">Always write internal links with url() from src/lib/site.ts (url("/visit#plan")), never a bare
"/visit", so they keep working when the site is built under a base path.
Features#
Service status and countdown#
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. Elements with data-countdown (on the home page and
the sermons page; add data-dark on dark backgrounds) 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.
[data-this-sunday] elements show the date of the next Sunday with a service still to come.
Everything is computed by serviceState() in main.ts 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#
The player is built by playerMarkup() and initPlayer() (section 4 of main.ts). 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 (put files inpublic/audio/and use'/audio/room-at-the-table.mp3'with your base path, or a full URL from your 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. Remove the note by giving each sermon anaudioURL.
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", buttons have
labels that change 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() 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. 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#
/sermon?id=<id> shows one sermon; an unknown or missing id shows the latest sermon with a notice. The page
title and meta description are updated from the sermon. "Copy questions" copies the title, passage and
numbered questions to the clipboard (it needs https or localhost; elsewhere it says copying failed).
Related sermons are the rest of the series, then other sermons by the same speaker.
Because the site is static, there is one sermon page that reads ?id=. If you prefer one URL per sermon
(/sermons/room-at-the-table), add src/pages/sermons/[id].astro with getStaticPaths() over SERMONS;
the dates would then be fixed at build time, so set KEEP_DEMO_CURRENT = false first.
Events calendar#
initEvents() (section 7) 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. 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 detail is a native <dialog> (focus is trapped and Esc closes it; 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.
Groups finder#
initGroups() (section 8) 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. 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.
The ministry panels below the finder (and the ministry cards on the home page) are rendered at build time
from MINISTRIES in src/data/ministries.ts.
Plan your visit#
initVisit() (section 9) 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 shows which room they go to from
VISIT.rooms. Children in the last room (youth) count as an extra seat.
Giving#
initGive() (section 10) 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 for connecting a payment provider.
Scroll reveal and back to top#
Elements with .reveal fade in as they scroll into view; everything above the current element is revealed
too, so jumping to an anchor never leaves gaps. The js class that enables the effect is added by a small
inline script in Layout.astro's <head>, so nothing is hidden when JavaScript is off. With reduced motion,
nothing is hidden or animated. The back-to-top button is created by src/scripts/back-to-top.ts; delete that
file, its import in Layout.astro and the .back-to-top styles in global.css to remove it.
Forms#
All forms are validated by setupValidation() in section 11 of main.ts: 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."
| 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") |
index.astro |
Replaced with a thank-you message |
Plan your visit (#visit-form) |
visit.astro (/visit#plan) |
Validates contact details and each child's age; confirms the Sunday, time and seats |
Giving (#give-form) |
give.astro |
Validates the amount (min and max from GIVING), name and email; explains nothing was charged |
Contact (#contact-form) |
contact.astro |
Pre-filled from ?topic= and ?group=; thank-you message |
Prayer (#prayer-form) |
contact.astro (/contact#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. To collect submissions, post the form in its callback in initForms(),
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.
The site is built as static files, so there is no server code. If you deploy with an Astro adapter (Netlify,
Vercel, Node), you can instead post to your own endpoint, for example src/pages/api/contact.ts with
export const prerender = false, so only that route runs on the server; see the
Astro endpoints guide.
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('/api/create-gift', { method: 'POST', body: new FormData(form) })
.then((res) => res.json())
.then((data: { url: string }) => { window.location.href = data.url; });Recurring gifts (frequency is weekly or monthly) map to subscriptions in most providers. Many church
platforms also offer an embeddable giving form you can use instead of this one.
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 in src/data/sermons.ts to stop using it.
SEO and sharing#
Each page passes its title and description to Layout, which writes the <title>, meta description,
canonical URL and Open Graph and Twitter tags. The sermon page also updates its title and description from
the sermon shown. Canonical and og:image URLs are absolute, built from site in astro.config.mjs: set
SITE_URL when you build (or change the default there):
SITE_URL=https://www.yourchurch.org pnpm buildThe 404 page passes noindex, so it gets robots: noindex and no canonical URL. For a sitemap, add
@astrojs/sitemap and a robots.txt in
public/. For search, consider Church structured data (a schema.org type) with your address and service
times in a <script type="application/ld+json"> block on the home page.
Accessibility#
- Skip link, landmarks, one
<h1>per page and a logical heading order. aria-current="page"on the current navigation item; the mobile menu usesaria-expanded, closes withEscand when the window grows to desktop width.- Visible focus outlines on every control; radio and checkbox cards show focus on the card.
- The audio player is fully keyboard operable, with labelled buttons and a native range slider with spoken time.
- The calendar is a table with a caption and column headers; day buttons have full-date labels with the
number of events; the event detail is a modal
<dialog>. - Filter results and counts are announced with
role="status"; form errors are linked to their fields. - Illustrations have
<title>text (the map also has a<desc>); decorative SVG is hidden from screen readers. - Colour contrast meets WCAG AA;
prefers-reduced-motionturns off reveals, the live pulse and the river shimmer.
When editing, keep one <h1> per page, give every new input a <label>, and don't use apricot for text.
Building and deploying#
pnpm build # static site in dist/
pnpm preview # preview dist/ locallydist/ is plain HTML, CSS and JavaScript. Pages are written as visit.html rather than visit/index.html
(build.format: "file" in astro.config.mjs), and links point to /visit without a trailing slash. Netlify,
Cloudflare Pages and GitHub Pages serve /visit from visit.html automatically; Vercel does with
cleanUrls (below). 404.html is served for unknown URLs by most hosts.
- Netlify / Cloudflare Pages: build command
pnpm build, publish directorydist. - Vercel: import the repository; it detects Astro and static output needs no adapter. Add a
vercel.jsonwith{ "cleanUrls": true }so/visitservesvisit.html. - GitHub Pages: use the official Astro GitHub Action.
For a project site (
username.github.io/repo), build withDEMO_BASE_PATH=/repo. - Any static host: upload the contents of
dist/. If the host doesn't map/visittovisit.html, changebuild.formatto"directory"inastro.config.mjs.
To serve the site from a sub-folder, set DEMO_BASE_PATH when building, for example
DEMO_BASE_PATH=/church pnpm build. It becomes Astro's base, and every link and image written with url()
(in pages and in main.ts) includes it. astro.config.mjs also sets compressHTML: false, so the spaces
between words and inline tags that start on a new line are kept.
demo/ (if present) is the build used for the freeframe.dev demo; you can delete it.
TypeScript 7 setup#
package.json installs two TypeScript packages on purpose. @typescript/native is TypeScript 7 and provides
the tsc that pnpm typecheck runs. typescript is an alias for @typescript/typescript6, the TypeScript 6
API that astro check and typescript-eslint load. When those tools support TypeScript 7, replace both with a
single typescript@7. tsconfig.json extends Astro's strict preset.
Troubleshooting#
- The countdown is an hour off. Check
CHURCH.timeZoneinsrc/data/church.tsis 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 player says playback is simulated. The sermon's
audiois empty. Add a URL to an audio file; if it is on another domain, that server must allow it to be played (most podcast hosts do). - A new sermon or group doesn't show under a filter. Its
series,speaker,book,day,areaorstagemust be a key of the matching export;pnpm checkwon't catch a wrong key, so compare spellings. - Links break when the site is in a sub-folder. A link was written as
"/page"instead ofurl("/page"). - "Copy questions" fails. The clipboard needs
httpsorlocalhost. - A class used only in TypeScript has no effect. Tailwind scans
src/; write class names in full ('bg-river-light', not'bg-' + colour) so it can find them. - Fonts look different offline. The fonts come from Google Fonts; without a connection the system fallback is used.
License#
MIT: free for personal and commercial use. See LICENSE.txt in the download. The SVG artwork, logo,
portraits and social image are part of the template and covered by the same license. Scripture quotations
in the demo are from the King James Version, which is in the public domain.