Overview#
Solace is a multi-page website template for a therapy and counseling practice. The demo is Budd Bay
Counseling, a fictional group practice at 412 Legion Way SE in Olympia, Washington, with six clinicians
(a psychologist, an LICSW, an LMFT, two LMHCs and a supervised associate) who see adults, couples, teens,
children and families in person and by video. This is the Astro version: Astro 7, TypeScript and Tailwind CSS
v4, with no UI framework. The practice data is in src/data/solace.ts and the interactions in
src/scripts/main.ts. It matches the HTML version page for page. This is version 1.0.0 (see template.json
and CHANGELOG.md). Live demo:
freeframe.dev/templates/solace.
The demo never goes stale: office hours, holidays, the next free consultation, reply-by dates and group start dates are all worked out in the browser from today's date in Pacific time, from rules rather than fixed dates.
Mental health copy needs care. The template keeps to general information, makes no promises about outcomes, shows no client testimonials or reviews, asks people not to put health details in forms, and points to the real crisis routes (call or text 988, call 911) on every page. Keep it that way when you edit, and have your own clinicians review the wording.
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. Add ?now=2027-01-04T10:00 to any page's URL to preview it at a
Pacific date and time (see Office hours, open now and holidays).
| 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/solace into demo/ (only used for the demo on freeframe.dev) |
Pages#
| File (route) | Purpose | Notable sections and features |
|---|---|---|
src/pages/index.astro (/) |
Home | Hero with next free consultation, how it works, services, clinician teaser, what to expect, fees at a glance, groups starting soon, crisis panel, map with open-now hours |
src/pages/approach.astro (/approach) |
Our approach | Ten approaches (CBT, ERP, EMDR, DBT-informed skills, ACT, Gottman, EFT, play, family, mindfulness) in tabs, each listing who offers it; #emdr opens one |
src/pages/services.astro (/services) |
Services and groups | Adults, couples, teens/children/families and video sessions, each with the clinicians who offer it; groups with rolling start dates and an interest-list form |
src/pages/team.astro (/team) |
Our clinicians | Directory with filters (synced to the URL), saved shortlist |
src/pages/clinician.astro (/clinician) |
Clinician profile | ?id=delacorte: bio, own words, weekly hours, next free consultation, fees, plans, training, related clinicians |
src/pages/fees.astro (/fees) |
Fees and insurance | Fee table, sliding-scale calculator, insurance checker with out-of-network estimate, Good Faith Estimate |
src/pages/match.astro (/match) |
Find a therapist | Five-question quiz that suggests up to three clinicians with reasons |
src/pages/faq.astro (/faq) |
Questions and answers | Searchable accordion in five topics; #money opens a topic |
src/pages/contact.astro (/contact) |
Request a consultation | Three-step request with live summary, office hours and holiday notice, reply-by date, map and directions |
src/pages/404.astro (404.html) |
Not found | Friendly message and links |
Project structure#
solace/
├─ src/
│ ├─ pages/ one .astro file per page (see above)
│ ├─ layouts/
│ │ └─ Layout.astro <head> (title, meta, Open Graph, fonts), skip link, header, footer, toast, script
│ ├─ components/
│ │ ├─ SiteHeader.astro crisis bar and panel, logo, main menu, mobile menu
│ │ └─ SiteFooter.astro footer columns: crisis note, office hours, address, links, copyright
│ ├─ data/
│ │ ├─ solace.ts practice data: PRACTICE, HOURS, HOLIDAYS, CLINICIANS, FEES, SLIDING, PLANS, GROUPS, WHO, MATCH…
│ │ └─ nav.ts menu items (mainNav, mobileNav)
│ ├─ lib/
│ │ └─ site.ts site name, theme color and the url() helper for links
│ ├─ scripts/
│ │ └─ main.ts all interactions (reads src/data/solace.ts)
│ └─ styles/
│ └─ global.css Tailwind theme (@theme tokens) and component classes
├─ public/img/
│ ├─ favicon.svg arch, sun and tide-line logo mark
│ ├─ hero-sound.svg home hero: dusk over Budd Inlet with cedars and a heron
│ ├─ svc-*.svg service art: individual, couples, families, groups, telehealth
│ ├─ approach-stones.svg, fees-shells.svg, heron-404.svg page art
│ ├─ portrait-*.svg six illustrated clinician portraits (320 x 400)
│ ├─ map.svg illustrated map of downtown Olympia (home and contact)
│ └─ og-image.svg, og-image.png social image source and the PNG used in og:image (1200 x 630)
├─ astro.config.mjs site URL, base path, output format, Tailwind plugin
├─ eslint.config.mjs, tsconfig.json
├─ package.json, pnpm-workspace.yaml, pnpm-lock.yaml
└─ README.md, DOCS.md, CHANGELOG.md, template.jsonPages are rendered to HTML at build time. src/scripts/main.ts is the only JavaScript sent to the browser, plus
one inline line in the layout that adds the js class to <html> before the page paints. Layout.astro imports
it and Astro bundles it, with the data it imports from src/data/solace.ts, into one small module.
Customizing#
Brand and colors#
The design tokens are in the @theme block at the top of src/styles/global.css. Every color becomes a Tailwind
utility (bg-clay-600, text-fir-800, border-line...).
| Token | Use |
|---|---|
--color-clay-50 … -800 |
Brand terracotta. clay-600 for primary buttons, clay-700 for links and eyebrows |
--color-sage-50 … -700 |
Calm fills, "accepting" pills, selected states |
--color-fir-600 … -950 |
Deep ink green: crisis bar, footer, dark panels, secondary buttons |
--color-tide-100, -300, -600 |
Water blue for artwork and small accents |
--color-ink, --color-body, --color-muted |
Heading, body and secondary text |
--color-oat, --color-oat-dark, --color-cream, --color-line |
Backgrounds and borders |
To change the brand color, replace the clay scale; pnpm dev picks it up:
@theme {
--color-clay-600: #7b4a8c; /* buttons */
--color-clay-700: #643b72; /* links, eyebrows */
}Keep white text on clay-600 and fir-800 at 4.5:1 contrast or more. A few colors are also written inside
the SVG files and the logo mark (in SiteHeader.astro and SiteFooter.astro); change those by hand if you rebrand.
Typography#
Headings use Gilda Display (an editorial serif, regular weight only) and body text uses Rosario
(a humanist sans, weights 400–700 plus italic). They load from Google Fonts with a <link> in
src/layouts/Layout.astro:
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Gilda+Display&family=Rosario:ital,wght@0,400..700;1,400&display=swap" />The stacks are --font-display and --font-sans in src/styles/global.css. To swap fonts, change the link and
the two tokens.
Content#
Page copy is in the .astro pages. Everything the pages calculate from lives in src/data/solace.ts, one
exported constant each (main.ts imports them with import * as D from "../data/solace"):
| Constant | What it holds | Used on |
|---|---|---|
PRACTICE |
Name, timeZone: 'America/Los_Angeles', phone, email, intake coordinator, refPrefix: 'BBC', replyBusinessDays: 1, consultLeadDays: 2 |
every page |
HOURS |
Session hours by weekday (0 = Sunday, null = closed) |
open-now pill, today's row |
PHONE_HOURS |
Front-desk days; also defines business days for reply-by dates | contact, groups |
HOLIDAYS, EXTRA_CLOSED |
Federal holiday rules and one-off closures ('YYYY-MM-DD') |
hours, groups, consultations |
CLIENTS, CONCERNS, FORMATS, TIMES |
Labels for who, what, how and when | filters, quiz, profiles |
CLINICIANS |
One object per clinician (below) | team, profiles, quiz, contact, home |
MODALITIES |
Approach names, keyed like the tabs on /approach |
profiles |
FEES |
Fee rows with CPT codes, three tiers, group and workshop fees, cancellation | fees, profiles, quiz |
SLIDING |
Poverty guideline base and per-person amount, fee bands | fees |
PLANS, OON |
Insurance plans and the out-of-network estimate | fees, team, quiz, contact |
GROUP_RULES, GROUPS |
Groups and their start-date rules | services, home |
WHO, MATCH |
Quiz answers and points | match |
The hours lists in the markup (data-hours, in SiteFooter.astro and on the home and contact pages) are
written out; keep them in step with HOURS.
Adding a clinician
Add an entry to CLINICIANS and a 320 x 400 portrait to public/img/. The key is used in URLs
(/clinician?id=underhill), so use lowercase letters only.
underhill: {
name: 'Sorrel Underhill', first: 'Sorrel', short: 'Sorrel', credential: 'LMHC',
role: 'Licensed mental health counselor', pronouns: 'they/them', tier: 'licensed',
clients: ['adults'], concerns: ['anxiety', 'grief'], modalities: ['act', 'cbt'],
formats: ['inperson', 'video'], days: { 1: ['09:00', '17:00'], 4: ['12:00', '20:00'] },
accepting: 'yes', insurance: ['soundshore'], sliding: false,
languages: ['English'], since: 2020,
focus: 'One line for cards', summary: 'Two sentences for the directory.',
bio: ['Paragraph one.', 'Paragraph two.'], words: 'A sentence in their own words.',
training: ['MA, Counseling', 'LMHC, Washington State'], art: 'portrait-underhill.svg'
}tierpicks the row ofFEES.tiers(psychologist,licensedorassociate).daysare session hours. A shift that starts beforeDAYTIME_UNTIL('15:00') counts as daytime, one that ends afterEVENING_FROM('17:00') counts as evenings, and day 6 counts as Saturdays.acceptingis'yes'or'waitlist'; a waitlisted clinician needswait: 'about 6 weeks'.insurancelists keys ofPLANS;sliding: truemeans they hold sliding-scale spots.
The directory, profile, quiz, contact form, approach tabs and service lists all pick the new clinician up. Also mention them in the home and team page copy where the number of clinicians appears ("Six therapists").
Fees and the sliding scale
FEES.tiers holds the fee per row id (consult, intake, individual, couples, extended). The table on
the fees page is built from it. The figures repeated in copy ($110, $165, $190 on the home page; $55 groups;
$75 late cancellation) are written in the pages.
The sliding scale works out income as a share of the federal poverty guideline:
guideline = base + perPerson × (household − 1), then the first band whose upTo percentage is not exceeded
sets the fee. Update base and perPerson with each year's guidelines.
| Household | Income | Share of guideline | Fee |
|---|---|---|---|
| 1 | $15,000 | 96% | $40 |
| 1 | $30,000 | 192% | $60 |
| 3 | $45,000 | 169% | $60 |
| 4 | $120,000 | 373% | $125 |
| 2 | $100,000 | 473% | Standard fee |
Insurance plans
PLANS entries have a type: 'in' (in network with the clinicians who list the plan), 'eap' (an employee
assistance program with sessions free sessions) or 'oon' (out of network: superbill). The plan names are
fictional placeholders; use the plans your practice really works with. The out-of-network estimate is
OON.allowed × share: with the defaults, 60% of $140 is about $84 back, so a $165 session costs about $81.
Groups
Each group in GROUPS starts on the nth weekday (0 = Sunday) of each month in months. The date shown is
the first one at least GROUP_RULES.minLeadDays (7) days from today that isn't a holiday (observed, or the calendar date of a fixed-date holiday), so the list never
shows a past date. weeks: 0 marks a one-day workshop. Opened on Nov 1, 2026 the demo shows Slack Tide from
Thursday, Dec 10; Little Hours from Saturday, Jan 2, 2027; Steady Ground from Tuesday, Jan 5, 2027; Tide Pool
from Wednesday, Jan 6, 2027; and Two Oars on Saturday, Mar 20, 2027.
To use fixed dates on a real site, replace the rule with your own logic in nextGroupStart() in src/scripts/main.ts, or
simply edit months each term.
Images and artwork#
All artwork is SVG in public/img/, drawn for this template: the hero landscape, five service illustrations, page art, the map
and six portraits. Each is an <img> (written with url("/img/…") so it works under a base path), so you can replace any of them with a photo of the same aspect ratio:
| Image | Size | Notes |
|---|---|---|
hero-sound.svg |
560 x 640 | shown in an arch; keep the subject centered |
svc-*.svg |
360 x 220 | cropped to 5:4 on the services page |
portrait-*.svg |
320 x 400 | shown in arches; keep faces in the upper middle |
approach-stones.svg, fees-shells.svg, heron-404.svg |
about 400 x 440 | page heroes |
map.svg |
640 x 440 | replace with a map image or an embedded map |
The social image is og-image.png (1200 x 630), generated from og-image.svg. Edit the SVG, then export it
to PNG with any tool (or open it in a browser and take a 1200 x 630 screenshot). The favicon is
favicon.svg.
Navigation, header and footer#
The crisis bar and header are src/components/SiteHeader.astro; the footer is src/components/SiteFooter.astro.
The menu items are in src/data/nav.ts: mainNav for the desktop menu (<nav aria-label="Main">) and
mobileNav (the same with Home first) for the mobile menu (#mobile-menu). Each page passes current to
<Layout> and the matching link gets aria-current="page".
To add a page, create src/pages/<name>.astro with <Layout title=… description=… current="<id>"> and a
<main id="main" tabindex="-1">, then add { id, label, href } to mainNav. Write internal links with url()
from src/lib/site.ts (href={url("/fees")}), never a bare "/fees", so they keep working when the site is served
from a sub-folder; in main.ts, links and images go through link().
The footer copyright year is filled in by main.ts (<span data-year>); the 2026 in the markup is only a fallback.
Features#
Crisis panel#
Every page starts with a bar: "In crisis right now? Call or text 988, any time. In an emergency, call 911."
The "More ways to get help now" button (data-crisis-toggle) opens a panel (#crisis-panel) with call
buttons and the note that the practice can't respond to emergencies by form or email. It is a disclosure
button with aria-expanded; Esc closes it while focus is inside. The home page, footer, match page and
contact form repeat the same routes. Don't remove them, and check the numbers if you adapt the template for
another country.
Office hours, open now and holidays#
renderHours() in src/scripts/main.ts highlights today's row in every data-hours list, fills the open-now pills
(data-open-status), shows the next holiday within 14 days (data-holiday-note) and the reply-by line
(data-reply-by). It runs every minute.
Holidays are the 11 US federal holidays, worked out for any year from the rules in HOLIDAYS. Fixed-date
holidays move to the Friday before when they fall on a Saturday, and to the Monday after on a Sunday, so
New Year's Day 2028 (a Saturday) is observed on Friday, Dec 31, 2027. Add one-off closures to EXTRA_CLOSED.
All dates use PRACTICE.timeZone (America/Los_Angeles), whatever the visitor's own time zone. For review,
add ?now=2027-01-04T10:00 to any page URL to see it at that Pacific wall-clock time.
Next free consultation and reply-by dates#
nextConsult(id) returns the first day at least consultLeadDays (2) days ahead when an accepting clinician
works and it isn't a holiday; waitlisted clinicians show their wait instead. The home hero shows the earliest
across the team (on Sunday, Nov 1, 2026: "Tue, Nov 3 with Eamon"). Reply-by dates are the next business day
(Monday to Friday, not a holiday) after today.
Clinician directory and shortlist#
/team filters the cards by concern, client group, in-network plan, format, "accepting new clients" and
"sliding-scale spots". Filters are written to the URL (/team?concern=trauma&format=video&accepting=1),
so filtered views can be shared; values are checked against the data before use. Example: concern trauma and
format video shows Eamon only; plan Cascadia Mutual with accepting shows Eamon and Nadira.
The "Save" buttons (data-save-id) are toggle buttons with aria-pressed that add a clinician to the
shortlist bar, which links to the consultation form with the first saved clinician. The profile page has the
same button.
Clinician profiles#
/clinician?id=<key> renders one clinician from CLINICIANS: name, role, pronouns, status, formats,
bio, "in their own words", a Monday to Saturday hours grid, the next free consultation, an "at a glance" list
and three related clinicians (most shared concerns and client groups). Without id it shows the first
clinician; an unknown id shows a "We couldn't find that clinician" message.
Approach tabs#
/approach uses the ARIA tabs pattern (role="tablist", arrow keys, Home and End). The URL hash selects a
tab (/approach#emdr), and profile pages link there. "Who offers it here" lists clinicians whose
modalities include the tab's key.
Match quiz#
/match asks five questions: who it's for, up to three concerns (MATCH.maxConcerns), format, times and
payment. matchClinicians(answers) in src/scripts/main.ts (also available as window.SOLACE.match):
- Keeps clinicians who see the chosen client group (
WHO[who].client). - Drops those who don't offer the chosen format, unless that leaves nobody (then it says so).
- Scores 3 points per matching concern, 1 per matching time, 2 for payment (in-network plan, sliding-scale
spots or the EAP) and 2 for accepting new clients (
MATCH.points). - Shows the top three, ties in data order, each with its reasons and a link to
/contact?clinician=<key>&from=match.
| Answers | Suggestions (score) |
|---|---|
| Me · anxiety, life changes · video · evenings · sliding scale | Eamon (11), Nadira (8), Iver (8) |
| Me and my partner · relationships · in person · Saturdays · Sound Shore Health | Isaura (8) |
| My teen · intense emotions, school stress · video · evenings · Cascadia Mutual | Nadira (11), Dr. Ostrander (3) |
| Me · trauma, grief · in person · daytime, evenings · EAP | Eamon (12), Iver (9), Isaura (4) |
| My child · anxiety · video · daytime · paying myself | Coralie (4), shown for in-person sessions |
Sliding scale and insurance checker#
On /fees, the calculator updates as you type and highlights the matching band; ?household=3&income=45000
prefills it. The plan checker (?plan= is kept in the URL) shows in-network clinicians, EAP details or the
out-of-network explanation with a refund estimate by share (50–80%).
FAQ#
/faq uses <details> elements, so answers open with the keyboard and without JavaScript. The search box
filters questions and answers as you type (?q=insurance prefills it) and announces the count. "Open all
answers" toggles every item, and linking to a topic (/faq#money) opens its first answer.
Mobile menu, reveals, back to top and toast#
The menu button has aria-expanded and aria-controls, and Esc or a click outside closes it. Sections fade
in once with an IntersectionObserver (off with reduced motion). A back-to-top button appears after scrolling;
toast() announces shortlist changes through a polite live region.
Forms#
There are two forms, and neither sends anything until you connect it:
| Form | Page | Validates | Success state |
|---|---|---|---|
Consultation request (data-intake-form) |
/contact |
Who, format and payment (plan if insurance); times and contact method; first and last name, email, a 10-digit US phone when contact is by phone or text, and age confirmation | "Thank you, Sam. Your request is in.", a reference like BBC-7K2QH, who will contact you and by when, and what happens next |
Group interest list (data-group-form) |
/services |
Group, first name, email | "You're on the list for Steady Ground", the start date, the reply-by date and a reference |
The consultation form reads ?clinician=, ?who=, ?format=, ?pay= and ?plan= from the URL and, with
?from=match, the saved quiz answers. Choose a clinician and it says whether they are accepting, their next
free consultation, and whether they work in the chosen format or with the chosen client group.
To connect a form service such as Formspree, post the form data in the submit handler and show the success
panel only when the service answers OK. In src/scripts/main.ts, find the comment "Connect your form service here" in the
consultation handler:
fetch("https://formspree.io/f/your-id", {
method: "POST",
body: new FormData(intake),
headers: { Accept: "application/json" },
}).then((r) => {
if (r.ok) { /* the existing success code */ }
else toast("Sorry, that didn’t send. Please call us on (360) 555-0142.");
});Do the same in the group form's submit handler. Choose a form service that offers a signed business associate agreement (BAA) if you might receive health information, and keep the form copy asking people not to include health details.
Saved data (localStorage)#
| Key | Shape | Validation on read | Cleared |
|---|---|---|---|
solace-shortlist |
array of clinician keys, e.g. ["delacorte"] |
unknown keys and duplicates dropped | "Clear" on the shortlist bar |
solace-match |
{ who, concerns[], format, times[], pay, plan, done } |
every value checked against the data | "Start over" |
solace-intake |
{ who, format, clinician, pay, plan, times[], contact, voicemail } |
every value checked | when the request is sent |
Names, email addresses, phone numbers and the free-text note are never stored. All access goes through
safeStorage in src/scripts/main.ts: when localStorage is blocked (sandboxed iframes, strict privacy settings), it
falls back to memory, so the shortlist, quiz and form keep working for the page view. To reset everything,
clear the site's data in the browser or run localStorage.clear() in the console.
SEO and sharing#
Each page passes its title and description to <Layout>, which writes the <title>, meta description,
canonical link, Open Graph tags (og:title, og:description, og:url, og:image, og:site_name, og:locale
en_US), a Twitter card and theme-color. The clinician page also updates document.title for the chosen
clinician. The 404 page passes noindex.
Canonical and Open Graph URLs are absolute, built from site in astro.config.mjs: set SITE_URL when you build
(SITE_URL=https://www.yourpractice.example pnpm build) or change the default there. After editing
public/img/og-image.svg, regenerate the PNG (1200 x 630) with any SVG-to-PNG tool, keeping the Gilda Display and
Rosario fonts available so the text renders.
Suggestions: add @astrojs/sitemap and a public/robots.txt; give each clinician a static page if search traffic
to them matters (/clinician?id= is one page; Astro's getStaticPaths() can generate one per key from
CLINICIANS); if you add structured data, the MedicalBusiness or Psychologist schema.org types fit.
Accessibility#
- Skip link, landmarks, one
h1per page and a logical heading order. - Visible focus outlines; WCAG AA color contrast for text.
- Keyboard-accessible menu, crisis disclosure, tabs,
<details>accordions, filters and toggle buttons witharia-pressed. - Radio and checkbox cards are real inputs inside labels, grouped in
<fieldset>with<legend>. - Inline errors are tied to their fields with
aria-describedbyandaria-invalid; focus moves to the first problem. Step changes move focus to the new step's heading. - Results and counts are announced with
role="status". prefers-reduced-motionturns off reveals, the floating cards and smooth scrolling.
When editing, keep one h1, label every new input, and keep the crisis routes visible.
Building and deploying#
pnpm build # static site in dist/
pnpm preview # preview dist/ locallydist/ is plain HTML, CSS and JavaScript. Pages are written as fees.html rather than fees/index.html
(build.format: "file" in astro.config.mjs), and links point to /fees without a trailing slash. Netlify,
Cloudflare Pages and GitHub Pages serve /fees from fees.html automatically; Vercel does with cleanUrls.
- 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/feesservesfees.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/feestofees.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=/counseling pnpm build. It becomes Astro's base; every link written with url() and every link
and image the script builds includes it. Set SITE_URL to your domain for canonical and Open Graph URLs. Use HTTPS,
and connect the forms before you go live.
Tailwind CSS is compiled by @tailwindcss/vite during pnpm dev and pnpm build; there is no separate CSS step.
It scans the .astro and .ts files for class names, so keep each class name whole in main.ts (not built from
pieces).
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#
- Times or "open now" look wrong. Everything uses
PRACTICE.timeZone. If your practice isn't in Pacific time, change it and update the "Pacific time" wording in the pages. - A new Tailwind class has no effect. Restart
pnpm devand hard-refresh. Classes used only insrc/scripts/main.tsmust be written as whole strings so Tailwind can find them. - A new clinician doesn't appear in the quiz. Check that their
clientsinclude the answer's client group (WHO), and that theirformatsinclude the chosen format. - The group date is not what you expected. It's the first date at least 7 days ahead in a listed month that
isn't a holiday; preview other dates with
?now=YYYY-MM-DDTHH:MM. - The shortlist or quiz answers don't survive a reload. The browser is blocking storage (private mode, a sandboxed preview); they still work for the page view.
- The social image doesn't show when shared. Build with
SITE_URLset to your domain soog:imageis the right absolute URL. - Links or images break when the site is in a sub-folder. Build with
DEMO_BASE_PATH=/<folder>and write internal links withurl(); inmain.ts, uselink().
License#
Freeframe Commercial License: use it in unlimited websites and apps for yourself and your clients; it may not
be resold or redistributed as a template. The full terms, with your licensee details, are in LICENSE.txt and
at https://freeframe.dev/license#commercial. Third-party components keep their own licenses. The fonts (Gilda
Display and Rosario) are from Google Fonts under the SIL Open Font License. Budd Bay Counseling, its
clinicians, insurance plans, groups and address are fictional placeholders.