Freeframe

Documentation

Smile

v1.0.1Updated Oct 15, 2026A bright, friendly website for a family and cosmetic dental practice, with online booking, a cost estimator and an emergency symptom guide.

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.

  1. Download smile.zip from freeframe.dev/templates/smile (the WordPress tab).
  2. 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.
  3. 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.
  4. 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:

sh
pnpm install
pnpm dev     # builds the CSS, then starts WordPress with the theme at http://127.0.0.1:9400

pnpm 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.txt

Customising#

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".

  • reveal fades a block in as it scrolls into view; a class such as [--d:80ms] delays it.
  • link-card on 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):

js
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:

js
{ 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, and CLINICIANS[...].img in 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 the img paths in the script (a full URL works) so the booking flow shows the same picture.
  • map.svg is 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.svg in an Image block next to the Grin Lane name in the header and footer patterns. The social image is assets/img/og-image.png (1200 × 630, source og-image.svg); the favicon assets/img/favicon.svg is used until you set a Site Icon in Settings > General.

Icons are Icon blocks: select one and pick another from the Smile collection or WordPress's own.

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 in REASONS.
  • Clinician is built from the visit type's who list, plus "First available". Each option shows the next opening.
  • Date and time. slotsFor(reasonId, clinician, dateKey) builds start times every BOOKING.slotStep minutes inside each clinician's working windows (practice HOURS on their days, minus LUNCH on long days), keeping only times where the whole visit (minutes) fits before the window ends. Times before now plus the visit's notice are skipped, as are CLOSED_DATES and anything beyond BOOKING.daysAhead. With "First available", each time is offered by the first free clinician in who order 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 to 0 when 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=emergency preselects a visit type; /book/?clinician=reyes preselects 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.adultYear per adult, MEMBERSHIP.kidYear per child), items covered by included free up to their yearly count per member, and everything else at MEMBERSHIP.discount off.

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):

  1. The main problem (one of TRIAGE.symptoms).
  2. Red flags (checkboxes; "None of these" clears the others). A flag listed as '911' in TRIAGE.flags sends people straight to the "Call 911" outcome; 'call' sends them to "Call us now".
  3. A yes or no follow-up question from the symptom's ask. Symptoms with ask: null go straight to their outcome.

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.

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 the nervous pattern's Custom HTML.
  • The FAQ on the Treatments page is a set of Details blocks (native <details>, class faq): duplicate one to add a question. The round "+" that turns into "×" is drawn by CSS (.faq > summary::after in src/theme.css).
  • Blocks with reveal fade 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-expanded and 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.js and the .back-to-top styles in src/theme.css, then run pnpm 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:

js
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:

  1. 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. Set BOOKING.demoBusyShare to 0.
  2. In the data-booking-form submit handler, POST the visit type, clinician, date, time and details to your server, which creates the appointment in the PMS.
  3. 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/:

sh
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:

sh
wp i18n make-pot wp-content/themes/freeframe-smile wp-content/themes/freeframe-smile/languages/freeframe-smile.pot

Save 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:

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 build after 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.timeZone and HOURS in 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 who clinicians work on days that are open in HOURS, that the visit minutes fit inside the opening hours, and that the dates aren't in CLOSED_DATES or beyond BOOKING.daysAhead. Late in the day, today may have no times because of the visit's notice.
  • 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-filter values (and ffd-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.

Search documentation

Search the guides and every template's documentation