Overview#
Solace is a website for a therapy and counseling practice, built as a WordPress block theme. The
fictional practice is Budd Bay Counseling, a group of six clinicians (a psychologist, an LICSW, an
LMFT, two LMHCs and a supervised associate) at 412 Legion Way SE in Olympia, Washington, who see
adults, couples, teens, children and families in person and by video. The theme has a home page
with the next free consultation and groups starting soon; an approach page with ten approaches in
tabs; services with groups that start on rolling dates and an interest-list form; a clinician
directory with filters and a saved shortlist; a clinician profile page; fees with a sliding-scale
calculator and an insurance checker; a five-question match quiz; a searchable FAQ; a three-step
consultation request; and a 404 page. A crisis bar with 988 and 911 is on every 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/solace.
Headings, copy, cards, lists, buttons and FAQ answers are core WordPress blocks arranged in block
patterns, so you edit them in the block editor without touching code. The parts that are worked
out in the browser (the crisis panel, office hours and holidays, the team lists, the directory, the
profile, the approach tabs, the groups, the fee tools, the quiz and the forms) work as in the HTML
version: they are Custom HTML blocks driven by assets/js/main.js, which reads the practice's data
from assets/js/data.js. Colors and fonts are design tokens in theme.json: change them in
Appearance > Editor > Styles and the whole site follows. Every date is worked out in
America/Los_Angeles (Pacific time), whatever the visitor's own time zone is, from rules rather
than fixed dates, so the demo never goes stale.
Mental health copy needs care. The theme 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: WordPress 7.1 or newer and PHP 7.4 or newer. No plugins are needed.
- Download
solace.zipfrom freeframe.dev/templates/solace (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 Solace.
- Go to Appearance > Solace setup and click Create the demo pages. This creates the Home, Our approach, Services and groups, Our clinicians, Clinician profile, Fees and insurance, Find a therapist, Questions and answers and Request a free consultation 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.
- 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 > Solace pages) made of section patterns. Every section is also offered on its own in the block inserter under Patterns > Solace.
| Page | Slug | Page pattern | Sections (pattern files in patterns/) |
|---|---|---|---|
| Home | / |
page-home.php |
home-hero (next free consultation), how-it-works, home-services, home-team (built from CLINICIANS), what-to-expect, home-fees (fees at a glance, groups starting soon), crisis-help, home-visit (map, open now, office hours) |
| Our approach | /approach/ |
page-approach.php |
approach-header, approaches (tabs; #emdr opens one), common-ground, approach-cta |
| Services and groups | /services/ |
page-services.php |
services-header, service-individual (#individual), service-couples (#couples), service-families (#families), groups (#groups, with the interest-list form), service-telehealth (#telehealth), services-cta |
| Our clinicians | /team/ |
page-team.php |
team-header, directory (filters, shortlist, cards), teamwork |
| Clinician profile | /clinician/ |
page-clinician.php |
profile-header, profile-body, profile-related, profile-missing (all filled from CLINICIANS by ?id=) |
| Fees and insurance | /fees/ |
page-fees.php |
fees-header, session-fees (#session-fees), sliding-scale (#sliding), insurance (#insurance), good-faith-estimate (#gfe), fees-cta |
| Find a therapist | /match/ |
page-match.php |
match-header, match-quiz |
| Questions and answers | /faq/ |
page-faq.php |
faq-header (search box), faq (17 answers in five topics), faq-cta |
| Request a free consultation | /contact/ |
page-contact.php |
contact-header, consultation (the request form, summary, office hours, reply-by date), finding-us |
| 404 | any missing URL | templates/404.html |
404 |
The clinician profile works as in the HTML version: one page, filled in the browser from
assets/js/data.js. /clinician/?id=delacorte shows Eamon Delacorte; with no id it shows the
first clinician (Dr. Ostrander), and an unknown id shows "We couldn't find that clinician". Every
link the theme writes to a profile uses this address. WordPress 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-solace/
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 scripts (with the script's strings, page and image URLs),
editor styles, the `js` class script, theme color and favicon,
title separator, pattern categories
inc/icons.php registers assets/icons/*.svg for the Icon block
inc/icon-labels.php the icons' names shown in the editor
inc/block-styles.php button block styles
inc/site-setup.php Appearance > Solace setup: creates the demo pages and the menu
templates/ page.html (pages), page-with-title.html, single.html, index.html,
archive.html, search.html, 404.html
parts/ header.html and footer.html (they show the header and footer patterns)
patterns/ every section and page (PHP files with block markup)
assets/css/theme.css the compiled stylesheet (do not edit: built from src/)
assets/css/editor.css the same for the block editor (generated by src/editor-css.mjs)
assets/js/data.js the practice's data (window.SOLACE): PRACTICE, HOURS, HOLIDAYS,
CLINICIANS, FEES, SLIDING, PLANS, GROUPS, MATCH and more
assets/js/main.js every interaction (the HTML version's script plus a WordPress adapter)
assets/icons/ the icons used by Icon blocks (outline SVG paths)
assets/img/ logo.svg and logo-light.svg (header and footer), favicon, social image,
hero-sound.svg, svc-*.svg, portrait-*.svg, map.svg and page art
assets/fonts/ Gilda Display and Rosario, with their licenses
src/theme.css the Tailwind CSS source of assets/css/theme.css
src/icons.css icons drawn before text in buttons with CSS (generated)
src/editor-css.mjs writes assets/css/editor.css after each build
languages/ freeframe-solace.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.txtCustomizing#
Brand and colors#
The colors are in theme.json (settings.color.palette) and in Appearance > Editor > Styles >
Colors > Palette:
| Color | Default | Used for |
|---|---|---|
| Clay 50 to Clay 800 | #fbf2ed … #6a3221 |
the brand terracotta: Clay 600 (#a04e32) for primary buttons, Clay 700 for links and eyebrows |
| Sage 50 to Sage 700 | #f1f4ee … #435440 |
calm fills, "accepting" pills, today's hours, selected states |
| Fir 600 to Fir 950 | #3b5a50 … #12221d |
the deep green of the crisis bar, footer, dark panels and secondary buttons |
| Tide 100, 300, 600 | #e3ecef, #a9c0c8, #4f6f7c |
water blue in artwork and small accents |
| Ink, Body, Muted | #1d2f2a, #3c4742, #5b6660 |
heading, body and secondary text |
| Oat, Oat dark, Cream, Line | #f3ecdf, #e9dfcd, #fbf8f2, #e2d8c7 |
backgrounds and borders |
| White, Black | for your own blocks |
The theme's stylesheet reads these colors from the palette (bg-clay-600, text-muted...), so
changing Clay 600 in Styles recolors every primary button at once. 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 (the
illustrations, assets/img/logo.svg, assets/img/logo-light.svg and assets/img/favicon.svg) and
in a few small drawings in src/theme.css (the eyebrow wave, the menu underline, the FAQ plus);
change those by hand if you rebrand.
Components worth knowing in src/theme.css: h-display and section-title (Gilda Display
headings), lead, eyebrow (labels with a tide-line mark; eyebrow-light on dark), numeral,
card, card-soft, card-hover, arch (the arched image frame), pill (pill-yes, pill-wait,
pill-plain), tick-list (a list with drawn ticks), tide-rule, contours, icon-disc, chip
and text-link. Add them to a block in Block settings > Advanced > Additional CSS class(es).
Typography#
Two fonts are bundled in assets/fonts/ and declared in theme.json
(settings.typography.fontFamilies), so no request goes to Google: Gilda Display (regular;
preset display) for headings and Rosario (400 to 700, plus italic; preset sans) for
everything else. 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. 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 to select a section, move it, duplicate it or delete it. To add a section from another page, use the block inserter (+) > Patterns > Solace.
What the script fills in or drives 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 |
|---|---|---|
header |
the crisis bar and panel, the menu button | see Crisis panel |
footer |
the tide-line edge, the office hours list, the toast | see Office hours, open now and holidays |
home-team |
the clinician cards | built from CLINICIANS |
home-fees |
groups starting soon | built from GROUPS |
home-visit, consultation |
the open-now pill and the hours list | see Office hours, open now and holidays |
approaches |
the tab list and each tab's "Who offers it here" list | see Approach tabs |
service-* |
"Clinicians who see …" lists | built from CLINICIANS |
groups |
the group cards, the interest-list form and its success panel | see Groups |
directory |
the filters, the shortlist bar and the cards | see Clinician directory and shortlist |
profile-* |
the portrait, pills, buttons, bio, quote, week, facts and related cards | see Clinician profiles |
session-fees, sliding-scale, insurance |
the fee table, the calculator and the plan checker | see Sliding scale and insurance checker |
match-quiz |
the quiz and the suggestions | see Match quiz |
faq-header |
the search box | see FAQ |
consultation |
the request form, the success panel and the summary | see Forms |
| several | the contour backgrounds and the tide drawing in the crisis section | decorative |
Some blocks carry classes (Block settings > Advanced > Additional CSS class(es)) that the script
reads. Blocks can't store data- attributes, so the script turns classes starting with ffd- into
them when the page loads: ffd-next-consult on the hero's floating card becomes
data-next-consult, ffd-faq-item on each Details block becomes data-faq-item, ffd-reply-by on
the contact page becomes data-reply-by, classes starting with ffa- become other attributes
(ffa-role--tabpanel becomes role="tabpanel"), and ff-hidden becomes the hidden attribute.
Spans inside text keep their attributes as they are (<strong data-accepting-count> in the hero is
filled with "4 of 6 clinicians"). Keep those classes when you restyle a block.
revealfades a block in as it scrolls into view; a class like[--d:120ms]delays it.link-cardon a group makes the whole group clickable: the first link inside it stretches over the group (the service cards, the logo in the header and footer).
Everything data-driven is in assets/js/data.js, inside window.SOLACE, the same file as in the
HTML version:
| 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 | team, profiles, quiz, contact, home |
MODALITIES |
approach names, keyed like the tabs on the approach page | 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 |
Change data.js in a child theme (see Child themes) so an update doesn't overwrite
it. The office hours lists (in the footer and the home-visit and consultation patterns) and the
fees repeated in copy ($110, $165, $190 on the home page; $55 groups; $75 late cancellation) are
written in the blocks; keep them in step with HOURS and FEES. "Six therapists" appears in the
home and team copy.
Adding a clinician
Add an entry to CLINICIANS and a 320 x 400 portrait to assets/img/ (in a child theme, see
Child themes for the image folder). 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.
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 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 |
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.
Images and artwork#
All artwork is SVG drawn for this template; there are no photos. The hero, service, page and map illustrations are Image blocks, so you can replace any of them with a photo of the same aspect ratio (select the block, Replace):
| 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 |
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 |
portrait-*.svg |
320 x 400 | used by the script (art in CLINICIANS); keep faces in the upper middle |
The portraits are loaded by the script from the theme's assets/img/ folder (their URLs are passed
in window.freeframeSolace.images by freeframe_solace_image_urls() in inc/setup.php). The social image is assets/img/og-image.png
(1200 x 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 crisis bar (a Custom HTML block); keep the crisis routes visible,
and check the numbers if you adapt the theme for another country.
The header has two Navigation blocks showing the same menu: the desktop one (from 1280 px) and the
one in the mobile menu, which has its own Home link above it. Edit the menu once in Appearance >
Editor > Navigation (the setup creates "Main menu": Approach, Services, Our team, Fees &
insurance, Find a therapist, FAQ, Contact). WordPress marks the current page's link with
aria-current="page", and the script marks the mobile menu's Home link on the home page (the page
URLs are in freeframe_solace_page_urls() in inc/setup.php). 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 footer's "Start here" links are a List block: click a link to change it, press Enter to add one.
The copyright year is written by PHP. The phone number and email appear in the mobile menu, the
footer and several pages; search for them when you change them (and in PRACTICE in data.js).
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; Escape closes it while focus is
inside. The home page, footer and contact form repeat the same routes.
Office hours, open now and holidays#
renderHours() in main.js 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, in the footer
and on the contact page) and the reply-by line (data-reply-by). It runs every minute. On Sunday,
Nov 1, 2026 the holiday note reads "Closed Wednesday, November 11 for Veterans Day." and the reply-by
line "Requests sent now get a reply by 5 pm on Monday, November 2."
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. Add one-off closures to EXTRA_CLOSED. All dates use PRACTICE.timeZone. 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.
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, so the list never shows a past date. 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. "Join the interest
list" on a card picks that group in the form below. To use fixed dates on a real site, replace the
rule with your own logic in nextGroupStart() in main.js, or edit months each term.
Clinician directory and shortlist#
The team page 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. 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 ("1 saved clinician", "Request a consultation with Eamon"), 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
("Next free consultation: Tuesday, November 3" for Eamon on Nov 1, 2026), an "at a glance" list and
three related clinicians. The approach links in the list open that tab on the approach page.
Approach tabs#
The approach page 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. The tab panels are blocks: edit their text in
the editor. The tab buttons are one Custom HTML block; to add an approach, add a button with
data-tab and aria-controls matching a new panel group's HTML anchor (panel-<key>).
Match quiz#
The match page asks five questions: who it's for, up to three concerns (MATCH.maxConcerns),
format, times and payment. matchClinicians(answers) in main.js (also available as
window.SOLACE.match) keeps clinicians who see the chosen client group, drops those who don't
offer the chosen format (unless that leaves nobody), scores 3 points per matching concern, 1 per
matching time, 2 for payment and 2 for accepting new clients (MATCH.points), and shows the top
three with their 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 the fees page, the calculator updates as you type and highlights the matching band;
/fees/?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#
The answers are Details blocks with the classes faq (which draws the round plus) and
ffd-faq-item, so they open with the keyboard and without JavaScript; add one by duplicating an
existing answer. 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. The topic groups have the class ffd-faq-group and an HTML
anchor.
Mobile menu, reveals, back to top and toast#
Below 1280 px the menu button (data-menu-button, aria-expanded, aria-controls="mobile-menu")
opens the mobile menu; Escape or a click outside closes it. Blocks with reveal fade in once as
they scroll into view (off with reduced motion). A back-to-top button appears after scrolling;
toast() announces shortlist changes through a polite live region in the footer.
Block styles#
In the block settings' Styles panel:
| Block | Styles |
|---|---|
| Button | Fill (default, the clay button), Fir green, Outline (fir), Light (on dark), Outline (on dark) |
The classes is-small and is-large on a Button block make it the smaller or larger size. Icons
before button text are drawn with CSS (icon-before-calendar, icon-before-phone,
icon-before-compass, icon-before-map-pin, from src/icons.css).
Forms#
There are two forms, and neither sends anything until you connect it. They are in Custom HTML blocks.
| Form | Pattern | Validates | Success state |
|---|---|---|---|
Consultation request (data-intake-form) |
consultation |
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) |
groups |
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 assets/js/main.js (in a child theme), 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(function (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. You can also replace a form's Custom HTML block with
a form plugin's block; add the classes label, field and btn btn-primary to its labels, fields
and button so it matches. 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 |
These are the HTML version's keys. Names, email addresses, phone numbers and the free-text note are
never stored. All access goes through safeStorage in main.js: 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.
SEO and sharing#
Page titles come from WordPress: the page title, an em dash and the site name (for example
"Fees and insurance — Budd Bay Counseling"); the home page shows the site name and tagline
(Settings > General). The profile page also sets document.title for the clinician shown. 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 x 630 image) as
the default social image. WordPress already generates a sitemap at /wp-sitemap.xml. If you add
structured data, the MedicalBusiness or Psychologist schema.org types fit.
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, visible focus
outlines, a keyboard-accessible menu, crisis disclosure, tabs, Details answers, filters and toggle
buttons with aria-pressed, radio and checkbox cards that are real inputs grouped in <fieldset>
with <legend>, inline errors tied to their fields with aria-describedby and aria-invalid,
focus moved to the first problem and to each new step's heading, results and counts announced with
role="status", 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), label
every field you add, keep aria-controls and aria-describedby ids in step, and keep the crisis
routes visible.
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; keep each class name whole in the script (not built from pieces).
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-solace, without node_modules/) and upload it in
Appearance > Themes, or copy the folder to wp-content/themes/ on your server. Use HTTPS, and
connect the forms before you go live.
Translations#
The theme is translation-ready with the text domain freeframe-solace. All text in patterns,
templates and PHP goes through WordPress's translation functions; the script's own labels and
messages (day names, form labels, statuses, validation errors) are translated in PHP
(freeframe_solace_script_strings() in inc/setup.php). Some sentences the script builds from
pieces (dates, reply-by lines, quiz reasons) are written in English in main.js; translate them
there. The practice's content in assets/js/data.js (clinicians, approaches, plans, groups) is
edited there directly. Create a translation with Loco Translate, or with WP-CLI and Poedit:
wp i18n make-pot wp-content/themes/freeframe-solace wp-content/themes/freeframe-solace/languages/freeframe-solace.potSave freeframe-solace-es_ES.mo (for example) in wp-content/languages/themes/ or in the theme's
languages/ folder. Page content created by Solace 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 Solace is updated (your clinicians, fees and
groups in particular). Create wp-content/themes/solace-child/style.css:
/*
Theme Name: Solace Child
Template: freeframe-solace
*/Templates, parts, patterns and theme.json in the child theme override the parent's files of the
same name. To use your own data.js, replace the parent's data script (freeframe-solace-data)
under the same handle in the child's functions.php:
add_action( 'wp_enqueue_scripts', function () {
wp_deregister_script( 'freeframe-solace-data' );
wp_register_script( 'freeframe-solace-data', get_stylesheet_directory_uri() . '/data.js', array(), '1.0', array( 'in_footer' => true, 'strategy' => 'defer' ) );
}, 15 );Do the same for assets/js/main.js (handle freeframe-solace), keeping the inline
window.freeframeSolace strings and URLs. images there lists the portrait URLs by file name; add
your own portraits to it. 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 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 > Solace.
- Times, "open now" or group dates look wrong. Everything uses
PRACTICE.timeZone; check for a leftover?now=in the URL, which freezes the date. If your practice isn't in Pacific time, change it and the "Pacific time" wording in the blocks. - Profile or consultation links go to the wrong page. The script links to
/clinician/,/contact/,/team/,/fees/and/approach/. If you renamed those pages, change their URLs infreeframe_solace_page_urls()ininc/setup.php(in a child theme, filter the inline script). - 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 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.
- Solace 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 > Solace pages.
License#
Split license: the PHP code and block markup are licensed under the GNU GPL, version 2 or later, and the CSS,
JavaScript, images and design under the Freeframe Commercial License (unlimited sites for you and your
clients; no resale or redistribution as a theme). 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.
Fonts: Gilda Display and Rosario are licensed under the SIL Open Font License 1.1 (OFL.txt in each folder of
assets/fonts/). Budd Bay Counseling, its clinicians, insurance plans, groups, address, phone number and
email address are fictional placeholders.