Overview#
Smile is a website for a family and cosmetic dental practice, built as a WordPress block theme. The
fictional brand is Grin Lane Dental, a practice in Boise's North End with four dentists, three
hygienists and a friendly front desk. The theme has a home page with Pip the tooth mascot, the next
available checkup, a same-day emergency banner, a $99 new patient offer, a treatments overview,
three reasons to choose the practice, the dentists, a review carousel, insurance and Grin Club
teasers, opening hours with an open-now badge and a map; a treatments page with category filters
and an FAQ; a pricing page with a fee table, the Grin Club membership, a cost estimator and a
payment-plan calculator; a team page filtered by role and spoken language; a new patients page with
the first visit step by step, a nervous-patient guide in tabs and a multi-step patient form; a
five-step booking flow; an emergency page with a symptom guide and first-aid steps; a contact page;
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/smile.
Headings, copy, cards, lists, buttons and images are core WordPress blocks arranged in block
patterns, so you edit them in the block editor without touching code. The interactive parts (the
booking flow, the estimator, the symptom guide, the patient form, the filters, the tabs, the review
carousel, the opening hours and the forms) work as in the HTML version: they are small Custom HTML
blocks driven by assets/js/main.js. Colours and fonts are design tokens in theme.json: change
them in Appearance > Editor > Styles and the whole site follows.
The content is general information for a fictional practice, not medical advice. Review all health copy, especially the emergency guidance, with a qualified professional before publishing your own site.
Quick start#
Requirements: WordPress 7.1 or newer and PHP 7.4 or newer. No plugins are needed.
- Download
smile.zipfrom freeframe.dev/templates/smile (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 Smile.
- Go to Appearance > Smile setup and click Create the demo pages. This creates the Home, Treatments, Pricing, Our team, New patients, Book online, Emergency and Contact 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 > Smile pages) made of section patterns. Every section is also offered on its own in the block inserter under Patterns > Smile.
| Page | Slug | Page pattern | Sections (pattern files in patterns/) |
|---|---|---|---|
| Home | / |
page-home.php |
home-hero (mascot, next checkup, rating), urgent (in pain right now?), new-patient-offer, treatments-preview, why-us (three illustrated cards and stats), team-preview, reviews (carousel), payment-teaser (insurance and Grin Club), visit-us (open-now badge, hours, map), home-cta |
| Treatments | /treatments/ |
page-treatments.php |
treatments-hero, treatments-list (category filter and sixteen treatments), treatments-faq |
| Pricing | /pricing/ |
page-pricing.php |
pricing-hero (section links), fees (fee table), membership (Grin Club), estimator, payment-plans, insurance |
| Our team | /team/ |
page-team.php |
team-hero, team-list (role and language filters, nine people), team-cta |
| New patients | /new-patients/ |
page-new-patients.php |
new-patients-hero, first-visit (steps and what to bring), nervous (tabs), patient-form, privacy |
| Book online | /book/ |
page-book.php |
book-hero, booking (the five steps and the summary) |
| Emergency | /emergency/ |
page-emergency.php |
emergency-hero (when to call 911 first), triage (symptom guide), first-aid, emergency-hours |
| Contact | /contact/ |
page-contact.php |
contact-hero (ways to reach us), contact-form (form, map, hours), getting-here (directions and accessibility) |
| 404 | any missing URL | templates/404.html |
404 |
Posts use templates/single.html; the blog, archives and search results use
templates/index.html, templates/archive.html and templates/search.html. For a plain text page
(a privacy policy, for example) choose the Page with title template.
Project structure#
freeframe-smile/
style.css theme header (name, version, requirements, license); no styles
theme.json design tokens: palette, fonts, spacing, layout widths, button
functions.php loads the files in inc/
inc/setup.php stylesheet and script (with its strings and the portraits' URLs),
editor styles, the `js` class script, theme colour and favicon, 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 > Smile 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 practice data, opening hours, booking, estimator, triage, forms...
assets/icons/ SVG icons for the Icon block
assets/img/ mascot, team portraits, map, logo, favicon, social image
assets/fonts/ Fredoka and Nunito, 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-smile.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 |
|---|---|---|
| Berry 50 to 950 | #f3f2ff to #1a1747 |
the brand blue-violet: buttons and links (600), dark sections and the footer (950) |
| Bubble 50 to 800 | #fff1f5 to #8e1c44 |
playful pink: emergency buttons and badges, tags |
| Lemon 50 to 700 | #fffbe6 to #8a6100 |
sunny highlight: the offer card, the squiggle underline, icon dots |
| Mint 50 to 800 | #ebfbf5 to #115545 |
calm green: the open-now badge, "included" tags, self-care states |
| Ink, Body, Muted | #1d1a4d, #403d68, #5e5b85 |
headings, body text and secondary text |
| Line, Cream, Cloud | #e9e6f5, #fffaf3, #f5f3ff |
borders, the page background and soft panels |
| White, Black | for your own blocks |
The theme's stylesheet reads these colours from the palette (bg-berry-600, text-mint-700...), so
changing Berry 600 in Styles recolours every primary button at once. To move to another brand
colour, change the whole Berry scale together and keep the 600 shade dark enough for white button
text (4.5:1). The illustrations (assets/img/*.svg) and the SVGs in Custom HTML blocks (the why-us
pictures, the page header blobs, the 404 tooth, the waves) carry their own hex colours: search for
the old values (for example #5146d4 for berry and #1d1a4d for ink) to change them too. The
browser UI colour (theme-color) is set in freeframe_smile_head_meta() in inc/setup.php.
Component classes from the HTML template work on blocks too: add them in Block settings >
Advanced > Additional CSS class(es). card gives a group the white card look, card-hover lifts
it on hover, icon-blob is the round blob behind an icon, tag with tag-berry, tag-pink,
tag-lemon or tag-mint makes a small pill, eyebrow is the white label above a heading,
section-title and display are the heading styles, lead the intro paragraph, squiggle (on a
span inside a heading) the yellow hand-drawn underline, and text-link the underlined bold link.
Typography#
Two fonts are bundled in assets/fonts/ and declared in theme.json
(settings.typography.fontFamilies), so no request goes to Google: Fredoka (variable, 400 to
700; preset display) for headings, and Nunito (variable, 400 to 900, with italics; 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 and --sans), so keep those slugs.
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 > Smile.
What the script drives, and pictures drawn in SVG, are 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 |
the dotted background, the open-now badge, the overlapping portraits, the star rating line | decorative pictures and the badge |
visit-us, emergency-hours, contact-form, contact-hero, footer |
the open-now badge and the opening hours | see Opening hours and open-now badge |
reviews |
the Previous and Next buttons and the six reviews | see Review carousel |
pricing-hero |
the section links and the $32 picture | links and a decorative picture |
fees |
the fee table | a table that stacks into cards on phones |
estimator, payment-plans |
the estimator and the calculator | see Cost estimator |
treatments-list, team-list |
the filter chips, the language filter, the live count and the empty message | see Filters |
nervous |
the tabs and their panels | see Tabs, FAQ and motion |
patient-form |
the whole form card | see New patient form |
booking |
the five steps and the summary | see Booking flow |
triage |
the symptom guide | see Emergency symptom guide |
contact-form |
the form and its thank-you message | see Forms |
why-us, urgent, new-patient-offer, membership, the page headers, 404 |
the illustrations, waves, circles and stars | SVG pictures |
header, footer |
the menu button; the footer wave and the toast | driven by the script; decorative |
Some blocks carry classes (Block settings > Advanced > Additional CSS class(es)) that the script
reads. Blocks can't store data- attributes or hidden, so the script turns classes starting with
ffd- into attributes when the page loads, and ff-hidden into hidden: ffd-tags--dentist on a
team card becomes data-tags="dentist", and ffd-langs--en_es becomes data-langs="en es".
revealfades a block in as it scrolls into view; a class such as[--d:80ms]delays it.link-cardon a group makes the whole group clickable: the first link inside it stretches over the group (the treatment cards on the home page, the logo).
Practice data that drives the interactive features lives in constants at the top of the second part
of assets/js/main.js, as in the HTML version:
| Constant | What it holds |
|---|---|
PRACTICE |
name, time zone (America/Boise), time zone label, phone and address used in the booking flow and the .ics file |
HOURS |
opening hours per weekday (0 = Sunday, null = closed), used by the open-now badge and the booking slots |
CLOSED_DATES |
whole-practice closures as YYYY-MM-DD |
LUNCH |
the lunch break on days longer than six hours |
CLINICIANS |
each clinician's name, short name, role, portrait and working weekdays |
REASONS |
visit types for booking: label, length in minutes, price note, who offers it and minimum notice |
BOOKING |
slot interval, how far ahead people can book and the demo "taken" share |
ESTIMATE_ITEMS, MEMBERSHIP, FINANCE |
estimator treatments and prices, Grin Club prices and discount, payment-plan terms |
TRIAGE |
symptom guide questions, red flags, tips and outcome wording |
The opening hours also appear as text in the Custom HTML hours lists (<dl data-hours>) in the
footer and on the home, emergency and contact pages. When you change HOURS, update those lists
too. Prices appear in the treatment cards, the fee table, the membership cards, the home page and
REASONS and ESTIMATE_ITEMS: keep them in step.
Adding a clinician
Add an entry to CLINICIANS and list its id in the who array of each visit type they offer (in a
child theme, see Child themes):
var CLINICIANS = {
// ...
park: { name: __('Dr. Grace Park'), short: __('Dr. Park'), role: __('Dentist'), img: asset('assets/img/dr-park.svg'), days: [1, 3, 5] }
};asset() looks the path up in the portrait URLs that freeframe_smile_script_assets() in
inc/setup.php passes to the script: add the new file's name to its list (or give img a full
URL, for example of an image in the Media Library). Then duplicate a card on the Our team
page in List View and change the portrait, name and text. The card's classes carry its filters:
ffd-tags--dentist (or hygienist, care) for the role chips and ffd-langs--en_ko for the
language menu (codes that match the <option> values in the team filter's Custom HTML; join several
with an underscore). Link straight to a clinician with /book/?clinician=park.
Adding a treatment
On the Treatments page, duplicate a treatment card and give it a category class ffd-tags-- with
one of general, cosmetic, orthodontics, implants, kids or emergency. Its booking button
links to /book/?reason=<id> with an id from REASONS. The chip counts are worked out by the
script; update the numbers in the chips' Custom HTML, which show before the script runs. If the
treatment should be in the estimator, add it to ESTIMATE_ITEMS:
{ id: 'veneer', name: __('Porcelain veneer'), price: 1150, qty: 0 },Add included: { per: 'adult', count: 2 } (or per: 'kid') when Grin Club covers it.
Images and artwork#
All artwork is SVG in assets/img/, used by Image blocks: select one and Replace it.
hero.svg(560 × 520) is Pip the tooth mascot on the home page; the two floating cards over it are blocks.- The team portraits (
dr-okafor.svg... 240 × 240) are Image blocks on the home and team pages, andCLINICIANS[...].imgin the script (the booking flow). To use photos, keep a square crop of at least 480 × 480 pixels, upload them to the Media Library, replace the Image blocks and change theimgpaths in the script (a full URL works) so the booking flow shows the same picture. map.svgis the illustrated map on the home and contact pages.- The why-us pictures, the page header blobs, the waves and the 404 tooth are inline SVG in Custom HTML blocks; replace any of them with an Image block if you prefer a picture.
- The logo is
assets/img/logo.svgin an Image block next to the Grin Lane name in the header and footer patterns. The social image isassets/img/og-image.png(1200 × 630, sourceog-image.svg); the faviconassets/img/favicon.svgis used until you set a Site Icon in Settings > General.
Icons are Icon blocks: select one and pick another from the Smile collection or WordPress's own.
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 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": Home, Treatments, Pricing, Our team, New patients, Contact, Dental emergency). Its first
link (Home) and last link (Dental emergency, with a lightning icon) are hidden in the desktop
header, which has the logo and its own "Emergency?" link (from 1280 px wide); keep them first and
last, or remove the .site-nav … :first-child and :last-child rules 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. WordPress marks the
current page in the menu; the script marks the logo, the "Emergency?" link and the "Book a visit"
buttons on their own pages.
The footer's Explore and Patients menus are List blocks: click a link to change it, press Enter to
add one. The address, phone number and email also appear on the contact page and in PRACTICE;
search for them when you change them.
Features#
Booking flow#
The Book online page has five steps: visit type, clinician, date and time, details and
confirmation, with a summary card beside them that follows every choice. The code is initBooking()
in assets/js/main.js; the markup is the booking pattern's Custom HTML (data-booking,
data-step and data-step-indicator).
- Visit type comes from the radio buttons named
reason. Their values must match keys inREASONS. - Clinician is built from the visit type's
wholist, plus "First available". Each option shows the next opening. - Date and time.
slotsFor(reasonId, clinician, dateKey)builds start times everyBOOKING.slotStepminutes inside each clinician's working windows (practiceHOURSon theirdays, minusLUNCHon long days), keeping only times where the whole visit (minutes) fits before the window ends. Times before now plus the visit'snoticeare skipped, as areCLOSED_DATESand anything beyondBOOKING.daysAhead. With "First available", each time is offered by the first free clinician inwhoorder and the slot shows their name. - Demo availability.
BOOKING.demoBusyShare(0.55) marks a stable, pseudo-random share of slots as taken so the demo looks realistic. Set it to0when you load real availability. - Details are validated (see Forms). The confirmation shows a summary, a clear demo notice and an "Add to calendar (.ics)" button that downloads a tentative event converted from Boise time to UTC.
- Links in.
/book/?reason=emergencypreselects a visit type;/book/?clinician=reyespreselects a clinician when they offer the chosen visit type.
All times are practice times (PRACTICE.timeZone), because patients come in person. To move the
practice, change timeZone and zoneName.
Accessibility: each step's heading receives focus when it opens; the progress bar marks the current
step with aria-current="step"; the calendar uses a roving tabindex (arrow keys move by day and
week, Home and End jump to the start and end of the week, Page Up and Page Down move four weeks) and
each day has a full label such as "Thursday, October 15, 2026, 6 times available". Unavailable days
use aria-disabled. Errors are announced next to the step's buttons.
Cost estimator#
On the Pricing page, initEstimator() renders a row with a stepper for each entry in
ESTIMATE_ITEMS, plus steppers for the number of adults and kids in the plan. It compares:
- Pay as you go: quantity × price for every item.
- With Grin Club: yearly membership (
MEMBERSHIP.adultYearper adult,MEMBERSHIP.kidYearper child), items covered byincludedfree up to their yearly count per member, and everything else atMEMBERSHIP.discountoff.
The cheaper option gets a "Best value" tag and a sentence explains the saving. Results are in an
aria-live region; each stepper is a labelled group with "Fewer" and "More" buttons and a number
input.
Payment-plan calculator#
Below the estimator (ffd-plan, which the script reads as data-plan), visitors enter a cost
between FINANCE.min and FINANCE.max (or use the slider, or "Use my estimate from above") and
choose a term from FINANCE.terms. The monthly payment uses the standard amortised loan formula, or
a simple division for 0% terms. The rates are deliberately labelled as made-up examples; replace
them with your financing provider's published terms or remove the section.
Emergency symptom guide#
The Emergency page has a three-question guide (initTriage(), data in TRIAGE):
- The main problem (one of
TRIAGE.symptoms). - Red flags (checkboxes; "None of these" clears the others). A flag listed as
'911'inTRIAGE.flagssends people straight to the "Call 911" outcome;'call'sends them to "Call us now". - A yes or no follow-up question from the symptom's
ask. Symptoms withask: nullgo straight to theiroutcome.
Outcomes are 911, call, today and self. Each shows its own steps plus the symptom's tip
and the matching buttons (elements with data-outcome-cta listing the levels they belong to). The
wording is intentionally cautious and the page repeats that it's general information, not a
diagnosis. If you change the rules, keep a route to emergency services for breathing problems,
spreading swelling and head injuries.
New patient form#
The patient-form pattern's Custom HTML holds five fieldsets (data-intake-step) with Back and Next
buttons, a progress bar, a "None of these" option that clears the other medical history boxes, a
typed signature that must match the first and last name from step 1, and a review screen with an
"Edit" button per section. See Forms for validation and connecting it.
Filters#
Treatments and team use one filter ([data-filter-root] in main.js): chips with data-filter
and aria-pressed, items with data-filter-item and data-tags, an optional select
(data-filter-select) matched against data-langs, an aria-live status message, live counts on
each chip, an empty state and a reset button. On blocks these attributes are ffd- classes (see
Content). The chip value is kept in the URL using the parameter named by
data-filter-param (category or role), so filtered views can be shared:
/treatments/?category=orthodontics or /team/?role=hygienist.
Opening hours and open-now badge#
renderHours() highlights today's row in every [data-hours] list and adds "(today)" for screen
readers. Every [data-open-status] badge shows "Open now · until 5:00 pm" or "Closed · opens
tomorrow 8:00 am", calculated in the practice's time zone (not the visitor's), and refreshes every
minute. The home page's "Next checkup" card (ffd-next-slot and ffd-next-slot-who) uses the
booking engine to find the first open checkup slot.
Review carousel#
The home page carousel is a horizontally scrolling list with scroll snapping; the Previous and Next
buttons move by one card and switch off at each end. It's keyboard-scrollable once focused and never
moves on its own. To add a review, copy an <li> in the reviews pattern's Custom HTML.
Tabs, FAQ and motion#
- The nervous-patient guide uses the ARIA tabs pattern (
role="tablist", arrow keys, Home and End); edit the panels in thenervouspattern's Custom HTML. - The FAQ on the Treatments page is a set of Details blocks (native
<details>, classfaq): duplicate one to add a question. The round "+" that turns into "×" is drawn by CSS (.faq > summary::afterinsrc/theme.css). - Blocks with
revealfade up into view; revealing one also reveals everything before it. The floating hero cards, the open-now pulse and the card hovers stop for visitors who prefer reduced motion, and nothing is hidden without JavaScript. - The header turns white with a shadow after scrolling. Below 1024 px the menu button opens the
mobile menu, updating
aria-expandedand its label; Esc closes it and returns focus to the button. - A back-to-top button appears after about one screen of scrolling. To remove it, delete the "Back to
top" part at the end of
assets/js/main.jsand the.back-to-topstyles insrc/theme.css, then runpnpm build.
Block styles#
In the block settings' Styles panel:
| Block | Styles |
|---|---|
| Button | Fill (default, the berry button), Outline, Lemon, Pink, Soft pink, White, Outline (on dark), Outline (on colour) |
The classes is-large and is-small on a Button block make it the hero or compact size. Icons on
buttons and links are classes too (icon-before-calendar, icon-after-arrow-right, with
icon-size-20); see src/icons.css for the available icons.
Forms#
Every form is a demo: it validates in the browser and shows a success state, but nothing is sent, booked or stored until you connect it. The confirmation screens say so.
Validation is driven by attributes in the forms' Custom HTML, handled by validateFields() and
fieldMessage() in assets/js/main.js:
| Attribute | Rule |
|---|---|
required |
must be filled, ticked or chosen (data-required-msg for a custom message) |
type="email" |
must look like an email address |
data-phone |
at least 10 digits |
data-past-date |
a valid date in the past (date of birth) |
minlength |
minimum length |
data-match |
must equal data-match-value (set by the patient form for the signature) |
data-validate |
opts the field into validation |
Errors appear under the field (or under the group for radio buttons), are linked with
aria-describedby, set aria-invalid and focus the first problem.
| Form | Page | Validates |
|---|---|---|
Contact form (data-contact-form) |
Contact | name, email, optional phone, topic, message of 10+ characters |
Booking details (data-booking-form) |
Book online | first and last name, email, mobile, new or returning, consent |
New patient form (data-intake) |
New patients | name, date of birth, phone, email, contact preference, visit reason, last visit, how nervous, medical history, consent and signature |
Connecting the contact form#
Use a form plugin (replace the form's Custom HTML block with the plugin's block; add the classes
label, field and btn btn-primary to its labels, fields and button), or send the form from the
script (in a child theme). In the [data-contact-form] submit handler in assets/js/main.js, send
it before showing the success state, for example with Formspree:
form.addEventListener('submit', function (e) {
e.preventDefault();
if (!validateFields($$('[data-validate]', form))) return;
fetch('https://formspree.io/f/your-form-id', { method: 'POST', body: new FormData(form), headers: { Accept: 'application/json' } })
.then(function (res) {
if (!res.ok) throw new Error('Send failed');
// ...the existing code that hides the form and shows [data-form-success]
})
.catch(function () { toast('Sorry, that didn’t send. Please call us instead.'); });
});Ask people not to include medical details in this form (the page already does).
Connecting the booking flow#
Bookings belong in your practice-management system (PMS). Most offer an online-scheduling API or an
embeddable booking widget. Either replace the booking section with the widget (a Custom HTML block
with its embed code), or keep this flow and:
- Load real open times from the PMS (through your own server, so API keys stay private) and return
them from
slotsFor()in the same{ mins, who }shape. SetBOOKING.demoBusyShareto0. - In the
data-booking-formsubmit handler, POST the visit type, clinician, date, time and details to your server, which creates the appointment in the PMS. - Show step 5 only after the PMS confirms, and change the confirmation copy to match.
Booking details are health information. Use HTTPS and a vendor that will sign a Business Associate Agreement (BAA) in the US or a Data Processing Agreement (DPA) under the GDPR in the EU and UK.
Connecting the new patient form#
The patient form collects health information, so a general-purpose form plugin, form service or plain email isn't appropriate:
- US (HIPAA): post it only to a HIPAA-eligible form or intake service that signs a BAA, or use your PMS's own patient intake forms and link to them instead.
- EU and UK (GDPR): health data is special category data. Use a processor with a DPA, keep the explicit consent checkbox in step 4, collect only what you need and state your retention period in your privacy notice.
Replace the success handling in initIntake() (the submit listener on [data-intake]) with a
POST to that service. Don't store the answers in WordPress (for example with a plugin that saves
form entries to the database) unless your hosting meets the same rules. The answers are deliberately
never written to localStorage or cookies; keep it that way.
SEO and sharing#
Page titles come from WordPress: the page title, an em dash and the site name (for example
"Pricing — Grin Lane Dental"); the home page shows the site name and tagline (Settings >
General). 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. A dental practice benefits from Dentist structured data (schema.org) with your
real name, address, phone, hours and geo-coordinates; most SEO plugins can add it. The theme doesn't
include it because it would describe a fictional practice.
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, the menu button with aria-expanded and a label that changes with its state, filter chips
with aria-pressed and a live count, accessible tabs, native details for the FAQ, a carousel that
never moves on its own, a keyboard-operable calendar, form errors announced through
aria-describedby and aria-invalid, focus moved to the first error or the next step's heading,
alt text or aria-hidden on every picture, 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 and don't skip heading levels, give images alt text (or
mark them decorative), keep link text meaningful, check contrast when you change colours (especially
white text on the 600 shades), label any field you add to a form (<label for="...">) and add
data-validate when it needs checking. Keep the emergency guidance visible and plain: it's the
content people need most when they're stressed.
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-smile, without node_modules/) and upload it in
Appearance > Themes, or copy the folder to wp-content/themes/ on your server. Serve the site
over HTTPS, especially once the forms are connected.
Translations#
The theme is translation-ready with the text domain freeframe-smile. All text in patterns,
templates and PHP goes through WordPress's translation functions; the script's strings (visit types,
clinicians, estimator rows, triage questions and outcomes, booking lines, form errors and messages)
are translated in PHP (freeframe_smile_script_strings() in inc/setup.php). Create a translation
with Loco Translate, or with WP-CLI and Poedit:
wp i18n make-pot wp-content/themes/freeframe-smile wp-content/themes/freeframe-smile/languages/freeframe-smile.potSave freeframe-smile-es_ES.mo (for example) in wp-content/languages/themes/ or in the theme's
languages/ folder. Page content created by Smile setup is copied into your pages in the site's
language at that moment; translate it in the editor like any other content. Dates and prices in the
booking flow and the estimator are formatted in US English by the script (toLocaleDateString and
toLocaleString with en-US).
Child themes#
Use a child theme for changes you want to keep when Smile is updated (your hours, clinicians, visit
types and prices in particular). Create wp-content/themes/smile-child/style.css:
/*
Theme Name: Smile Child
Template: freeframe-smile
*/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-smile) and enqueue yours in the child's functions.php, keeping the inline
window.freeframeSmile strings and portrait URLs (or copy them). 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 > Smile.
- The open-now badge looks wrong. It uses
PRACTICE.timeZoneandHOURSin the script, not the hours lists in the pages and not the visitor's clock. Check both and keep them in step. - The booking calendar shows no times. Check that the visit type's
whoclinicians work on days that are open inHOURS, that the visitminutesfit inside the opening hours, and that the dates aren't inCLOSED_DATESor beyondBOOKING.daysAhead. Late in the day, today may have no times because of the visit'snotice. - A new team member or treatment doesn't show up under a filter. Its group needs the class
ffd-tags--<value>with one of the chips'data-filtervalues (andffd-langs--for the language menu). See Adding a clinician. - Home or Dental emergency is missing from the desktop menu. The desktop header hides the menu's first and last links. See Navigation, header and footer.
- Smile 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 > Smile pages.
- The forms or the booking flow 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 mascot, team portraits, map, illustrations and
logo are included under the same license. Fredoka and Nunito are licensed under the SIL Open Font
License 1.1 (OFL.txt in each folder of assets/fonts/). The practice, people, reviews and prices
are fictional.