Freeframe

Documentation

Brief

v1.0.1Updated Nov 13, 2026A calm, plain-English website for a solo lawyer or attorney, with fixed fees, an options checker and booking.

Overview#

Brief is a website template for a solo lawyer or attorney. The demo content is for Pennington Employment Law, the fictional Dayton, Ohio, practice of independent employment attorney Tessa Pennington, who advises employees and senior staff on severance agreements, layoffs, wrongful termination, discrimination and problems at work, mostly for fixed fees. This is the Nuxt version: Nuxt 4, Vue 3 single-file components, TypeScript and Tailwind CSS v4, generated as a static site. This is version 1.0.1 (see CHANGELOG.md). Live demo: freeframe.dev/templates/brief.

The sample copy is general information about employment law in Ohio and under federal law, written for a fictional practice. Before you publish, have a qualified lawyer in your jurisdiction review every legal statement, fee, time limit and regulatory detail, and replace the placeholders (registration number, insurance, complaints procedure) with your own.

Quick start#

Requirements: Node.js 24 or newer and pnpm (corepack enable pnpm, or npm install -g pnpm).

sh
pnpm install
pnpm dev          # http://localhost:3000, reloads as you edit

Other scripts:

Command What it does
pnpm generate Generates the static site into .output/public/
pnpm preview Serves the last build locally
pnpm build A Node.js server build in .output/ (pages are still prerendered)
pnpm typecheck nuxt prepare and vue-tsc with Nuxt's strict settings
pnpm lint ESLint (the flat config from @nuxt/eslint)
pnpm build:demo The freeframe.dev demo build, copied to demo/

Pages#

File Route Purpose Notable sections and features
app/pages/index.vue / Home Hero with the plain-English promise, portrait and "Book a free 15-minute call", credentials strip, four areas of help, four-step "how working together works", fixed-fee teaser, client reviews with a disclaimer, FAQ accordion, call to action
app/pages/services.vue /services Services Eight services with what's included, typical timeline and fee (<ServiceList>); filter chips by area with live counts and a shareable ?area= URL
app/pages/fees.vue /fees Fees Fee estimator (<FeeEstimator>: matter, complexity, employer contribution, payment plan), full fee table, staged lawsuit fees, employer contributions, payment plans and insurance
app/pages/check.vue /check Check your options Branching questionnaire with plain-English results (<OptionsCheck>), and a claim deadline calculator (<DeadlineCalculator>)
app/pages/resources.vue /resources Guides Featured guide, topic filters, search, empty state, shareable ?tag= and ?q= URLs (<GuideLibrary>)
app/pages/article.vue /article Sample guide Long-form article with breadcrumb, reading progress bar, sticky table of contents, not-legal-advice notices, author box and related guides
app/pages/about.vue /about About Story, three promises, credentials timeline, office illustration and hours, regulation, insurance, complaints and confidentiality (#regulation)
app/pages/book.vue /book Book a call Four-step booking flow with a live summary sidebar (<BookingFlow>), plus a short message form (<ContactForm>)
app/error.vue 404.html Not found Struck-out page illustration and links back into the site

Project structure#

brief/
  nuxt.config.ts            base path, <head> defaults, SITE_URL, prerendered routes, Tailwind
  package.json              scripts and dependencies
  pnpm-workspace.yaml       a standalone project (allowed build scripts, overrides)
  tsconfig.json             references the configs Nuxt writes to .nuxt/
  eslint.config.mjs         ESLint flat config
  public/img/
    favicon.svg             logo mark, used as the favicon
    og-image.svg            social sharing image source (1200 x 630)
    og-image.png            the same image as PNG, referenced by og:image
    tessa-portrait.svg      illustrated portrait (480 x 560)
    office.svg              office and river illustration on the about page (640 x 460)
  app/
    app.vue                 <NuxtLayout><NuxtPage /></NuxtLayout>, with the page key (see "Links")
    layouts/default.vue     skip link, header, the page, footer and the back-to-top button
    pages/*.vue             one file per page (index, services, fees, check, resources,
                            article, about, book)
    error.vue               the 404 page
    components/
      SiteHeader.vue        logo, main menu, phone number, Book button and mobile menu
      SiteFooter.vue        link lists, contact details, the not-legal-advice notice,
                            the regulatory statement and copyright
      ServiceList.vue       service cards and area filter
      FeeEstimator.vue      the fee estimator
      OptionsCheck.vue      the "what are my options?" check
      DeadlineCalculator.vue  the claim deadline calculator
      GuideLibrary.vue      guide cards, topic filter and search
      BookingFlow.vue       the four-step booking flow and summary
      ContactForm.vue       the message form on the booking page
      FieldError.vue        an inline form error
      AppIcon.vue           a line icon from app/data/icons.ts
      BackToTop.vue         the back-to-top button
    composables/
      usePageSeo.ts         per-page title, description, canonical and Open Graph tags
      useValidation.ts      form validation rules and messages
      useAsset.ts           public/ paths with the base path
      useYear.ts            the copyright year
    data/
      matters.ts            matters and fees, estimator levels, payment plan
      check.ts              the options check questions and results, deadline rules
      booking.ts            call types and booking availability
      services.ts           the service cards
      guides.ts             the guide library
      areas.ts              areas of work (the services filter)
      icons.ts              line icons as SVG markup
    utils/
      dates.ts              date keys, federal holidays, deadline math, long dates
      zone.ts               Eastern-time math for booking slots
      format.ts             money(), plural(), makeRef() and setParams()
      safeStorage.ts        localStorage with an in-memory fallback
    plugins/
      reveal.client.ts      scroll reveals
      same-page.client.ts   links to the page you're on start it afresh
      stable-build.server.ts  reproducible builds
    router.options.ts       query strings written like the HTML version's links
    assets/css/main.css     Tailwind source: fonts, brand tokens (@theme) and component classes
    assets/fonts/           self-hosted Literata and Albert Sans (woff2) and OFL.txt
  README.md, CHANGELOG.md, DOCS.md

Customizing#

Brand and colors#

All colors are tokens in the @theme block of app/assets/css/main.css. Tailwind turns each one into utilities (bg-ink, text-pine, border-line and so on), so changing a token changes it everywhere.

Token Default Used for
--color-paper #f5f3ec Page background
--color-paper-2 #ece9df Sunken panels, tags
--color-card #fffdf8 Cards and inputs
--color-line / --color-line-strong #dcd8ca / #bfb9a6 Hairlines and input borders
--color-ink #13221c Text, primary buttons, dark sections
--color-ink-2 / --color-ink-3 #1c3229 / #2c473b Raised surfaces and hovers on ink
--color-slate #4c5852 Secondary text (AA on paper and card)
--color-slate-soft #b6c4bb Secondary text on ink
--color-pine / --color-pine-soft #1f5a43 / #dfe9e2 Links, icons and icon tiles
--color-lime / --color-lime-soft / --color-lime-deep #d4eb5c / #edf6bd / #b8d13a The highlighter accent: marks, chips, buttons on ink

The lime accent is a fill color. Don't use it for text on light backgrounds; use text-pine or text-ink. To change the accent, for example to a warm coral:

css
@theme {
  --color-lime: #ffb199;
  --color-lime-soft: #ffe1d6;
  --color-lime-deep: #f2896b;
}

The SVG files in public/img/ and the inline SVGs (logo, page illustrations) use the hex values directly; search for #d4eb5c and #13221c to update them too. The browser theme-color is in app.head.meta in nuxt.config.ts.

Typography#

Two variable fonts are self-hosted from app/assets/fonts/ and declared with @font-face at the top of app/assets/css/main.css (each in a latin and a latin-ext file, the same files Google Fonts serves):

  • Literata (--font-serif, used through font-serif and the .display class) for headings, prices and quotes. Weights 300 to 600, italic 300 to 500, with an optical-size axis that .display turns on.
  • Albert Sans (--font-sans) for everything else. Weights 400 to 700, italic 400 to 600.

To swap a font, put its woff2 files in app/assets/fonts/, change the @font-face rules and the two --font-* tokens. Both tokens include system fallbacks.

Content#

Most text lives directly in the page files in app/pages/. The data behind the interactive features lives in typed files in app/data/, imported with ~/data/<file>:

File and export What it holds Used by
matters.ts MATTERS Each matter's name, area, timeline, whether an employer contribution applies, three levels ([min, max] in US dollars) and three hints Fee estimator, options check results, booking topics
matters.ts LEVELS ["straightforward", "standard", "complex"], the ?level= values Fee estimator
matters.ts PLAN, DEFAULT_CONTRIBUTION { minFee: 1000, months: 3 }, and the starting contribution (500) Fee estimator
check.ts CHECK_STEPS, CHECK_RESULTS The questions of the options check and the 15 result screens Options check
check.ts CHECK_KEY "brief-check-v1", the localStorage key Options check
check.ts DEADLINE_RULES Claim types and their time limits in days or months Deadline calculator
booking.ts CALL_TYPES Booking call types: label, minutes, price, where Booking flow
booking.ts AVAILABILITY Time zone, weekly hours, notice, closed days Booking flow
services.ts SERVICES The service cards: id, area, icon, title, summary, what's included, timeline and fee Services page
guides.ts FEATURED, GUIDES, TOPICS The guide cards and the topic chips Guides page
areas.ts AREAS The four areas of work Services filter

TypeScript checks every entry (pnpm typecheck), so a typo in a new matter or result shows up in your editor.

To add a matter, add an entry to MATTERS:

ts
vacation: {
  name: "Vacation pay claim",
  area: "Claims and disputes",
  timeline: "1–2 weeks",
  contribution: false,
  levels: [[300, 300], [750, 750], [1000, 1400]],
  hints: ["Checking your figures", "Plus a letter to your employer", "Several years of underpayment"],
},

It then appears in the estimator's matter list and the booking topic list. Add a matching card to SERVICES in app/data/services.ts with the same id (links such as /services#vacation, /book?topic=vacation and /fees?matter=vacation#estimator use it), and a row to the fee table in app/pages/fees.vue.

Guides are entries in GUIDES with tags (topic ids from TOPICS), keywords (extra search words that aren't on the card), a tone for the card header (pine, paper or lime), an icon and an href.

Text in .vue templates is HTML: entities such as &rsquo; work as they are. A literal {{ in text must be written as {{ "{{" }}.

Images and artwork#

All artwork is SVG, so it stays sharp and light. Files in public/ are copied to the site as they are; pages reference them with asset() from useAsset() so the base path is included (:src="asset('img/office.svg')").

  • public/img/tessa-portrait.svg (480 x 560) is the portrait on the home and about pages, and is cropped to a circle or square on the article and booking pages. To use a photo, keep a 6:7 portrait crop and replace the file; the object-cover object-top classes keep the face in view in the small crops.
  • public/img/office.svg (640 x 460) on the about page.
  • Inline SVGs: the logo mark in the header and footer, the document drawings in the call to action, options check, featured guide and 404 page, and the icons. Icons drawn from data (service cards, guide cards) use <AppIcon name="..."> with the shapes in app/data/icons.ts.
  • public/img/og-image.svg is the source of the social image. Social networks need a PNG, so export it to public/img/og-image.png at 1200 x 630 after editing (any vector editor or a headless browser).
  • public/img/favicon.svg is the favicon (set in nuxt.config.ts).

The header and mobile menu are in app/components/SiteHeader.vue, the footer in app/components/SiteFooter.vue. Links are <NuxtLink to="/fees"> (no .html, no trailing slash), which adds the base path. Header links pass :aria-current="current('/fees')", which marks the page you're on with aria-current="page" (and draws the highlighter underline); on the article page, Guides stays marked. Other links pass :aria-current="undefined" so Nuxt doesn't mark them on its own.

To add a page, create app/pages/<name>.vue with a <main id="main" tabindex="-1"> root and a usePageSeo() call, add "/<name>" to routes in nuxt.config.ts (every page is prerendered from that list), and add links to the header menu, the mobile menu and, if you like, a footer list.

The footer holds the not-legal-advice notice and the regulatory statement. Keep both, and replace the placeholder registration number with your own.

In the HTML version every link loads a page. Here links switch pages in the browser, so app/plugins/same-page.client.ts makes a link to the page you're already on (the Services link on a filtered services page, a "Help with" link in its footer) start that page afresh at the linked address, scrolled to the top, as a page load would. Links that only jump to a #section of the current page are left alone.

Features#

Every feature is a component with its own state; each reads the URL and saved data in onMounted(), so the generated HTML always matches the first render in the browser.

Fee estimator#

app/components/FeeEstimator.vue, on the fees page (#estimator, [data-estimator]).

  • The matter list is built from MATTERS. The three level cards (input[name="est-level"], values 0-2) show the matter's hints.
  • When min equals max the result shows a fixed fee with a "Fixed fee" tag; otherwise a range with a "Range: fixed in writing before work starts" tag.
  • The breakdown always lists the fee, any employer contribution and what you pay.
  • My employer is contributing only appears for matters with contribution: true (severance agreements and exit negotiations). The amount (default DEFAULT_CONTRIBUTION) is deducted from the fee, never below zero.
  • I'd like to spread the cost shows PLAN.months equal payments when the amount you pay (after any contribution) is at least PLAN.minFee, and otherwise explains why it isn't available.
  • The matter and level are written to the URL (/fees?matter=tribunal&level=complex) with setParams(), so links such as "Estimate this fee" on the services page open the right estimate. Unknown values fall back to a severance agreement at the straightforward level.

The result is announced politely to screen readers (aria-live). Every control is a native select, radio, checkbox or number input, so it works with a keyboard.

Options check#

app/components/OptionsCheck.vue, on the check page ([data-check]).

Each step in CHECK_STEPS has a question q, a hint (or "") and options. An option has either next (another step id) or result (a key of CHECK_RESULTS). The first step is always start. A result has a tag, title, summary, steps, an urgent message (or null), a service (a MATTERS key, or null) and a deadline (a DEADLINE_RULES key, or null).

The result screen shows the summary and next steps, an amber "Time-sensitive" box when urgent is set, the recommended service with its lowest fee and links to the services page, estimator and booking (/book?topic=<service>), a "There may be a time limit" prompt whose link preselects the right claim type in the calculator (the component emits deadline, and app/pages/check.vue calls the calculator's select()), and a collapsible recap of the answers. Back and Start again buttons let visitors change their answers.

To add a branch, add a step and point an option at it:

ts
"transfer-stage": {
  q: "Has the transfer happened yet?",
  hint: "",
  options: [
    { label: "Not yet", result: "general" },
    { label: "Yes", result: "general" },
  ],
},

Each question is a <fieldset> with a <legend> that receives focus when it appears; answers are radio buttons styled as cards. A missing answer shows an inline error with role="alert".

Deadline calculator#

app/components/DeadlineCalculator.vue, on the check page (#deadline, [data-deadline]).

Each claim type in DEADLINE_RULES has days or months. A rule in days counts from the date (300 days from March 15 is January 9). A rule in months ends the day before the same date that many months later, or on the final day of that month if it has no such date (deadlineFrom() in app/utils/dates.ts). The two 300-day rules follow the EEOC charge deadline in Ohio; the two-year rules follow the Ohio Civil Rights Commission and federal wage limits. The result shows the date, the number of days left ("1 day", "12 days"), and warnings when the date is 21 days away or less, today, or has passed. Dates must be within the last two years or the next six months.

This is an estimate only and says so on screen. It doesn't model continuing violations, tolling or late-claim exceptions, and it doesn't store anything.

Booking flow#

app/components/BookingFlow.vue, on the book page ([data-booking]).

  1. Call type from CALL_TYPES. /book?type=advice preselects one.
  2. Date and time: a month calendar (Monday first) and time slots generated from AVAILABILITY: weekly hours windows in Eastern time, slotStep minutes apart, at least minNoticeHours from now, up to daysAhead days ahead, skipping US federal holidays (worked out for any year by federalHolidays() in app/utils/dates.ts) and any closed dates. demoBusyShare hides a stable share of slots so the demo looks realistic; set it to 0 when you connect a real calendar.
  3. Details: name, email, optional phone, topic (/book?topic=settlement preselects it), the conflict-check fields (employer, required; anyone else involved, optional), an optional deadline, a short description and a required acknowledgment that booking doesn't create a client relationship.
  4. Confirmation with a summary, a short reference (see Forms), what to do to change the time and a Save a calendar hold (.ics) button.

Times are always generated in America/New_York (app/utils/zone.ts), so they follow US daylight saving time. If the visitor's device is in another time zone, each slot also shows their local time, and so do the summary and confirmation.

The calendar is a table of buttons with a roving tabindex: arrow keys move by day and week, Home and End to the start and end of the week, Page Up and Page Down by four weeks. Unavailable days are aria-disabled, the chosen day is aria-pressed, and each day has a full label such as "Tuesday, October 20, 2026, 9 times available". Each step's heading receives focus when the step opens.

  • Services (app/components/ServiceList.vue): chips with data-filter show the cards in that area, update the counts and the "8 services shown" status, and keep the choice in ?area=.
  • Guides (app/components/GuideLibrary.vue): topic chips (data-tag) and the search box combine. Search matches every word against the card's text and its keywords. Both are kept in the URL as ?tag= and ?q=. When nothing matches, an empty state offers to clear everything.

Chips are buttons with aria-pressed, and the result counts are in a polite live region.

Article reading aids#

On the article page (app/pages/article.vue), [data-progress] is the bar at the top of the window; it fills as you scroll through [data-article]. The table of contents ([data-toc]) marks the section you're reading with aria-current="true". Each <h2> in the article needs an id that matches a table of contents link and an entry in the page's TOC list.

FAQ accordion, scroll reveals and motion#

The home page FAQ buttons (.acc-btn) toggle the panel named in aria-controls and update aria-expanded; any number can be open. Elements with .reveal fade in as they scroll into view (app/plugins/reveal.client.ts), and the hero highlighter (.draw-mark) draws in once. With "reduce motion" turned on, everything appears immediately. A head script in nuxt.config.ts adds the js class to <html>, so content is only hidden for the reveal when JavaScript runs. Don't put .reveal on an element with a :class binding: Vue would drop the is-visible class the plugin adds.

Header, mobile menu and back to top#

The header gains a border once you scroll. Below 1024px the menu button opens #mobile-menu; Escape closes it and returns focus to the button, and it closes when a link is chosen or the window grows past 1024px. The back-to-top button (app/components/BackToTop.vue) appears after about one screen of scrolling and hides while the footer's "Back to top" link is visible. To remove it, delete the file, its line in app/layouts/default.vue and the .back-to-top styles in app/assets/css/main.css.

Forms#

There are three forms. None of them sends anything until you connect it, even though the booking confirmation and the message form's thank-you read as if it had been. Both show a short reference (PE- and four characters) made in the browser by makeRef() in app/utils/format.ts; if your form service or booking tool returns its own reference, show that instead. They promise a reply or confirmation within one business day, as the rest of the booking page does: change them all if yours differs.

Form Where Validates
Booking details ([data-booking-form]) Book page, step 3 (BookingFlow.vue) Name, email, optional phone (10+ digits), topic, employer, description (20+ characters), acknowledgment
Message ([data-contact-form]) Book page (ContactForm.vue) Name, email, message (20+ characters)
Deadline calculator ([data-deadline]) Check page (DeadlineCalculator.vue) A date within the last two years or the next six months

Validation runs on submit and again as fields are corrected (app/composables/useValidation.ts). Each field declares its rule with data-validate (required, email, phone-optional, message, consent, date-event); the messages are in fieldMessage(). Errors appear under the field (<FieldError>), are linked with aria-describedby, set aria-invalid="true", and focus moves to the first invalid field.

Connecting to Formspree#

Create a form at Formspree, then replace the marked comment in submit() in app/components/ContactForm.vue:

ts
async function submit() {
  if (!v.validate(form.value)) return;
  const res = await fetch("https://formspree.io/f/your-id", {
    method: "POST",
    body: new FormData(form.value!),
    headers: { Accept: "application/json" },
  }).catch(() => null);
  if (!res?.ok) {
    alert("Sorry, your message could not be sent. Please email or call instead.");
    return;
  }
  // ...then show the thank-you panel as before (name, reference, sent.value = true)
}

The booking form has the same marked comment in submit() in app/components/BookingFlow.vue.

Taking real bookings#

The simplest route is a scheduling tool such as Cal.com or Calendly: replace the step 2 calendar in BookingFlow.vue with the tool's embed, or send people straight to your booking link from step 1. If you keep this calendar, post the details form to your form service (as above) and add the chosen slot:

ts
const data = new FormData(form.value!);
data.append("call_type", type.value);
data.append("start", slot.value!.at.toISOString());

Set AVAILABILITY.demoBusyShare to 0 in app/data/booking.ts, and add any other days off to closed.

Because inquiries can contain sensitive personal information, use a form service that stores data in a suitable region, and say in your privacy notice how you handle it. The site is static, so there is no server code to secure, but never put API keys or secrets in app/: everything there ends up in the browser.

Saved data (localStorage)#

Key Shape Written by
brief-check-v1 { "answers": [0, 1], "date": "2026-10-15" } Options check, when a result is reached

answers holds the chosen option index for each question in order. On load, replay() in OptionsCheck.vue replays the answers through CHECK_STEPS; if any index is out of range, or the answers don't end in a result, the saved value is ignored and the intro is shown. If you change the questions, change CHECK_KEY in app/data/check.ts (for example to "brief-check-v2") so old answers are dropped. Start again removes the key; visitors can also clear site data in their browser. Storage goes through safeStorage in app/utils/safeStorage.ts: when the browser blocks it, the result is kept in memory for the page view instead. Nothing else is stored, and nothing is sent anywhere.

SEO and sharing#

Each page calls usePageSeo({ title, description }) (app/composables/usePageSeo.ts), which outputs the title, meta description, a canonical link, Open Graph tags (og:title, og:description, og:type, og:site_name, og:url, og:image) and twitter:card. The article passes type: "article"; app/error.vue passes noindex, which outputs <meta name="robots" content="noindex"> and no canonical.

Canonical, og:url and og:image are absolute URLs built from runtimeConfig.public.siteUrl plus the base path. Set your domain when building:

sh
SITE_URL=https://www.example.com pnpm generate

or change the fallback (https://pennington-law.example) in nuxt.config.ts.

Suggestions: add a public/robots.txt and a sitemap (hand-written in public/, or a sitemap module), and add LegalService structured data (schema.org JSON-LD, with useHead()) to app/pages/index.vue with your real name, address, phone and business hours. The template doesn't include structured data because it would describe a fictional practice. Check your state bar's rules on advertising, reviews and how fees must be published.

Accessibility#

Built in:

  • Skip link, landmarks (header, nav, main, footer), one <h1> per page and a logical heading order.
  • Visible focus outlines; colors meet WCAG AA (body text slate on paper is about 7:1).
  • Native form controls with labels, hints linked by aria-describedby, inline errors and focus management.
  • Accessible patterns: mobile menu (aria-expanded, Escape), accordion, chips (aria-pressed), live regions for counts and the estimate, keyboard calendar, focus moved to new steps and results.
  • prefers-reduced-motion turns off reveals, the highlighter animation and smooth scrolling.

When editing, keep one <h1> per page, give every new input a <label>, and keep text on lime dark (text-ink).

Building and deploying#

sh
pnpm generate     # static site in .output/public/
pnpm preview      # serve it locally

nuxt generate prerenders every route listed in routes in nuxt.config.ts as <page>.html (fees.html, linked as /fees), plus 404.html from app/error.vue. Next to each page is a folder with its _payload.json, which Nuxt loads when you move between pages. Upload .output/public/ to any static host (Netlify, Cloudflare Pages, GitHub Pages, S3, Vercel). Hosts that don't map /fees to fees.html on their own need that rewrite (Vercel: "cleanUrls": true in vercel.json); most serve 404.html for unknown URLs. Serve the site over HTTPS, especially once forms are connected.

To serve the site from a sub-folder, set DEMO_BASE_PATH, which becomes Nuxt's app.baseURL:

sh
DEMO_BASE_PATH=/brief pnpm generate

pnpm build makes a Node.js server build instead (run it with node .output/server/index.mjs); the pages are still prerendered, and you can add server routes. demo/ is the freeframe.dev demo build and can be deleted.

Tailwind scans app/ for class names (and nuxt.config.ts, for the <body> classes). Classes that only appear inside TypeScript strings must be written out in full (for example "tag tag-amber"), not assembled from pieces.

TypeScript setup#

pnpm typecheck runs vue-tsc against the configs Nuxt writes to .nuxt/ (tsconfig.json references them). typescript is aliased to @typescript/typescript6, the TypeScript 6 API that vue-tsc and typescript-eslint load; replace it with typescript@7 once they support it.

Troubleshooting#

  • New Tailwind classes have no effect. Make sure the class appears in full in a file under app/. Restart pnpm dev if you added a new folder.
  • pnpm lint fails with a missing .nuxt/eslint.config.mjs. Run pnpm install (or pnpm typecheck) once; nuxt prepare writes it.
  • An option in the check leads nowhere. Every next must name a key of CHECK_STEPS, and every result a key of CHECK_RESULTS.
  • The check keeps showing an old result. It's saved in localStorage. Click Start again, or change CHECK_KEY after changing the questions.
  • The booking calendar has no free days. Check AVAILABILITY.hours (day 0 is Sunday), minNoticeHours, daysAhead and closed, and that demoBusyShare is below 1.
  • Booking times look an hour out. Slots are in America/New_York and follow US daylight saving time. Change AVAILABILITY.timeZone and zoneName if you practice elsewhere.
  • A new page shows the 404 page after pnpm generate. Add its route to routes in nuxt.config.ts.
  • A form shows its thank-you message but nothing arrives. It sends nothing until you connect it; see Forms.
  • The social preview has no image. Build with SITE_URL set to your real domain so og:image is an absolute https:// URL to og-image.png.

License#

Freeframe Commercial License: use it in unlimited websites and apps for yourself and your clients; it may not be resold or redistributed as a template. The full terms, with your licensee details, are in LICENSE.txt and at https://freeframe.dev/license#commercial. Third-party components keep their own licenses. Literata and Albert Sans are included under the SIL Open Font License (app/assets/fonts/OFL.txt). The practice, people, reviews and fees are fictional.

Search documentation

Search the guides and every template's documentation