Freeframe

Documentation

Hearth

v1.0.1Updated Oct 17, 2026A rustic, dog-friendly country inn website with rooms, menus, walks and a live price checker.

Overview#

Hearth is a complete website for a pub with rooms, a country inn or a bed and breakfast, shown as The Fallow Hart, a fictional old drovers' inn with six rooms on the green at Kirkby Malham in the Yorkshire Dales. It has a "fire's lit" status worked out from the opening hours, rooms with seasonal prices and a stay checker, tabbed menus with a dietary filter and a daily specials board, walks from the door with a route map, events, gift vouchers and directions. The look is rustic and warm: woodcut-style SVG artwork, cream paper, forest green and ox-blood, a slab serif and a chalkboard. This is the HTML + Tailwind CSS v4 version (1.0.0): plain HTML files, one stylesheet and one vanilla JavaScript file, with no build step needed to run it.

Live demo: freeframe.dev/templates/hearth

Quick start#

  1. Unzip the download.
  2. Open index.html in a browser. Every page and feature works straight from the file system.

To preview it the way a host serves it, run any static server in the folder:

sh
python3 -m http.server 8000   # then open http://localhost:8000

You only need Node.js 24 (for npm) if you change src/input.css or add Tailwind classes that aren't used yet (see Building and deploying).

Pages#

File Page Notable sections and features
index.html Home Woodcut hero with the "fire's lit" status card, a check-a-stay bar, today's hours and the next event, three rooms, food, walks, dogs, guest book, voucher ticket
rooms.html Rooms and prices Six rooms with features and a price per season, the stay checker (#check), the booking enquiry form and house notes
food.html Food & drink Menus in tabs (breakfast, lunch, dinner, Sunday roast), diet filter, chalkboard specials for today, ales on handpull
walks.html Walks from the door Eight walks with length, difficulty and saved filters, an SVG route map, save buttons
dogs.html Dogs & good to know Dog policy, what's provided for dogs, FAQ (<details>), house rules
events.html What's on Upcoming events for the next 60 days, filterable by type; the regulars; barn hire
vouchers.html Gift vouchers Voucher builder with live preview, character counter, delivery choice and total
contact.html Contact & getting here Contact form (topic from ?topic=), address, opening hours with today highlighted, directions and a sketch map
404.html Not found Friendly 404 page for your host to serve

Project structure#

hearth/
  index.html, rooms.html, food.html, walks.html,
  dogs.html, events.html, vouchers.html, contact.html, 404.html
  assets/
    css/style.css        compiled Tailwind CSS (generated, don't edit by hand)
    js/main.js           all interactions; settings and data at the top
    img/
      hero.svg           home page woodcut (inn, fells, walls, stag)
      room-*.svg         one woodcut per room (snug, hayloft, shepherd, beck, cove, dairy)
      food.svg           pie and pint
      hearth.svg         fireplace and sleeping dog
      fiddle.svg         events artwork
      favicon.svg        stag roundel
      og-image.svg       social image source
      og-image.png       social image (1200x630) used by og:image
  src/input.css          Tailwind source: brand tokens and components
  package.json           npm scripts to rebuild the CSS
  README.md, DOCS.md, CHANGELOG.md
  LICENSE.txt            MIT license (in the download)

Customising#

Brand and colours#

The design tokens are in the @theme block at the top of src/input.css:

Token Value Used for
--color-paper #f4ecdc Page background (cream paper)
--color-paper-2, --color-paper-3 #ebdfc6, #ddcca9 Panels and deeper panels
--color-ink #211b15 Text, borders, printed shadows
--color-muted #5c5043 Secondary text
--color-forest, --color-forest-2, --color-forest-dark #22402f, ... Status strip, green bands, footer, pressed filters
--color-oxblood, --color-oxblood-dark #7d2431, #5f1a24 Accent: buttons, eyebrows, errors
--color-ember #d9932f Firelight; decorative and on dark backgrounds only
--color-chalk #f2ede1 Text on green and on the chalkboard
--color-board #1e2622 Chalkboard

Change a value and rebuild the CSS. For example, a navy accent instead of ox-blood:

css
@theme {
  --color-oxblood: #23395b;
  --color-oxblood-dark: #172840;
}

The paper grain is an inline SVG noise tile on body (in @layer base); delete the background-image line for flat paper. The SVG artwork uses the same colours as fixed hex values, so edit the files in assets/img/ (or the inline SVGs in the pages) if you change the palette a lot.

Components defined in src/input.css include btn (with btn-primary, btn-green, btn-paper, btn-sm), card, panel, stamp, ticket, board, toggle, tab, field, field-error, eyebrow, display, press (the letterpress text shadow) and fleuron.

Typography#

Three Google Fonts are loaded by a <link> in the <head> of every page:

  • Zilla Slab (--font-display, class font-display): headings, buttons, labels.
  • Alegreya (--font-serif, the body font): text and italics.
  • Caveat Brush (--font-chalk, class chalk): the specials board only.

To swap one, change the Google Fonts URL in every page's <head> and the matching token in @theme.

Content#

Text lives in the HTML files. Data that features compute with lives in constants at the top of assets/js/main.js:

Constant What it holds
TIME_ZONE 'Europe/London': every clock-based feature uses the inn's time, not the visitor's
HOURS Per weekday (0 = Sunday): bar [open, close], kitchen list of [from, to], breakfast [from, to], all 'HH:MM'
SEASONS low (Nov to Feb), mid (Mar to May, Oct), high (Jun to Sep): a label and its months
ROOMS Per room id: name, sleeps, dogs (true if dogs allowed), rates per season in pounds
ROOM_ORDER Room ids in display order (also used by the demo availability)
STAY_RULES maxNights 14, maxDaysAhead 365, saturdayMinNights 2, maxGuests 4, maxDogs 2, dogFee 15
SPECIALS Per weekday: a list of [dish, price] for the chalkboard
EVENT_RULES Recurring events: weekday, optional nth weeks of the month (-1 = last), time, title, price, text, type
EVENTS One-off events with an ISO date; sameWeekday: true keeps the weekday when the demo moves it to a later year
KEEP_DEMO_CURRENT true: past one-off events come back a year later so the demo always has seasonal specials (see Events). Set it to false once EVENTS holds your real dates
EVENT_TYPES, EVENT_WINDOW_DAYS Type labels (quiz, music, food, special) and how far ahead to list (60 days)
WALKS Per walk id: name, miles, level; must match the cards on walks.html
WALK_LENGTHS, WALK_LEVELS Length buckets in miles (short under 4, half 4 to 8, full 8 or more) and level labels
VOUCHERS, VOUCHER_LIMITS Voucher kinds and prices; min 10, max 500, messageMax 150, postage 3
CONTACT_TOPICS Topics allowed in contact.html?topic=

Room prices appear in two places: the ROOMS constant (used by the stay checker) and the price tables in rooms.html and the "from" prices on index.html. Change both. To add a room:

js
var ROOMS = {
  // ...
  barn: { name: 'The Barn Loft', sleeps: 3, dogs: true, rates: { low: 150, mid: 170, high: 195 } }
};
var ROOM_ORDER = ['snug', 'hayloft', 'shepherd', 'beck', 'cove', 'dairy', 'barn'];

Then copy one <article id="room-..."> in rooms.html, add an <option value="barn"> to the #stay-room select, and draw or add an image.

Menus are plain HTML in food.html. Each dish is an <li data-dish data-diet="...">; the diet codes are v (vegetarian), vg (vegan) and gf (gluten-free), separated by spaces. Vegan dishes carry v vg so they also match the vegetarian filter:

html
<li class="grid grid-cols-[1fr_auto] gap-x-4 border-b border-dotted border-ink/30 py-3.5" data-dish data-diet="v vg">
  <div class="min-w-0"><h4 class="inline font-display text-lg font-bold">Lentil stew</h4>
    <abbr class="diet diet-vg ml-1 align-[2px] no-underline" title="Vegan">vg</abbr>
    <p class="mt-0.5 text-muted">Smoked paprika, kale, herb dumplings</p></div>
  <p class="font-display text-lg font-bold tabular-nums">&pound;13</p>
</li>

Walks are cards in walks.html (<li data-walk="id" data-miles="..." data-level="...">) plus a route path in the map (<path class="route" data-route="id" d="...">), and an entry in WALKS. The map uses a 600 x 520 coordinate space; the inn is at about (300, 380).

Events: add one-offs to EVENTS with an ISO date ('2027-05-01') and set KEEP_DEMO_CURRENT = false; past events then drop off by themselves.

Images and artwork#

All artwork is SVG drawn for this template in a woodcut style: hero.svg (1200 x 800, shown at 3:2), the room pictures and food.svg, hearth.svg, fiddle.svg (800 x 600, shown at 4:3). To use photos, replace the src of the <img> and keep the aspect ratio of its frame (aspect-[3/2], aspect-[4/3]); the art class crops with object-fit: cover. The walk map, the getting-here map and the 404 picture are inline SVG in their pages.

The social image is assets/img/og-image.png (1200 x 630), made from og-image.svg. The favicon is assets/img/favicon.svg.

The header and footer are repeated in every HTML file. To add a page, copy an existing one, change its <title>, description, Open Graph tags and data-page, then add a link to the desktop nav (<nav aria-label="Main">), the mobile menu (#mobile-menu) and, if you like, the footer lists in every page. Mark the current page with aria-current="page" on its links. The desktop nav shows from 1280px (xl); below that the menu button opens #mobile-menu.

The green strip above the header shows the short status ([data-status-short]), the phone number and the place.

Features#

Open status ("fire's lit")#

initStatus() reads the time in TIME_ZONE and the day's HOURS, then fills:

  • [data-status-title]: "The fire's lit" while the bar is open; "Morning at the Hart" from 5am until the bar opens; "The fire's banked for the night" otherwise. The site never shows a bare "Closed".
  • [data-status-detail] and [data-status-short]: bar closing time and kitchen state ("Kitchen serving until 9pm", "Kitchen opens at 6pm"), or when the bar opens next.
  • data-state on every [data-status] element: open (a glowing ember dot) or resting.
  • [data-today-bar], [data-today-kitchen], [data-today-breakfast], [data-today-name]: today's hours on the home page.
  • [data-hours-day] rows on contact.html: today's row gets data-today, aria-current="date" and a "Today" stamp.

Times are written the British way ("12 noon", "11:30pm"). The static text in the HTML is what shows without JavaScript. The opening hours table in contact.html is plain HTML: keep it in step with HOURS.

Check a stay (home page)#

#quick-check on the home page validates the arrival and departure dates with the same rules as the stay checker, then opens rooms.html?checkin=YYYY-MM-DD&checkout=YYYY-MM-DD&guests=N#check.

Stay checker and prices#

initStayChecker() drives #stay-check on rooms.html:

  • Validation (checkStay()): arrival is required, a real date, not before today in London and at most maxDaysAhead days away; departure is after arrival and at most maxNights nights later. If the stay includes a Saturday night it must be at least saturdayMinNights nights ("Try leaving on Monday 26 October."). When a room is chosen, guests must fit (sleeps) and dogs must be allowed. Errors appear under each field with aria-invalid.
  • Prices (quote()): each night is priced at the room's rate for that night's season, so a stay across October and November mixes mid and low rates. The breakdown shows one line per season ("3 nights x £145 (mid season)") and a dog line ("1 dog x £15 per stay"), then the total. Text uses the right singular or plural.
  • Availability (isBooked()) is a fixed demo pattern so some rooms show "Booked on Sat 24 Oct". Replace it with a call to your booking system.
  • With no room chosen, all six rooms are listed: available ones with prices and an "Enquire about ..." button that fills the enquiry form; others with the reason (too small, dog-free, booked).
  • URL parameters prefill the form and show prices at once: room (a room id), checkin, checkout (YYYY-MM-DD), guests (1 to 4), dogs (0 to 2). Room ids are checked with Object.prototype.hasOwnProperty.call(ROOMS, id), so ?room=constructor is ignored.

The results area (#stay-results) is a polite live region and receives focus after "Check prices".

Booking enquiry#

#enquiry sits under the checker. [data-enquiry-summary] repeats the chosen stay and total. The checker and the enquiry fields are saved as you type (see Saved data); "Clear saved details" ([data-clear-draft]) empties both forms and the saved copy. A successful (demo) send also clears it.

initMenu() on food.html follows the ARIA tabs pattern: arrow keys, Home and End move between tabs, only the selected tab is in the tab order. The hash selects a tab (food.html#roast; ids breakfast, lunch, dinner, roast, checked against the tabs on the page). Without a hash it opens what the kitchen is serving now in London: breakfast before 11am, Sunday roast on Sundays until 4pm, lunch until 5pm, dinner after.

The diet toggles ([data-diet-filter="v|vg|gf"], aria-pressed) combine: pressing vegan and gluten-free shows dishes that are both. Empty groups hide, [data-menu-empty] shows when nothing matches, and [data-diet-status] announces "Showing 3 of 13 dishes on the Dinner menu."

Specials board#

initSpecials() writes today's SPECIALS (London weekday) into [data-specials] and the day name into [data-specials-day]. Prices are formatted as £7.50 or £22.

Walks: filters, map and saved walks#

initWalks() on walks.html:

  • Length ([data-filter-length]: any, short, half, full) and difficulty ([data-filter-level]: any, easy, moderate, strenuous) filter the cards; "Saved walks only" ([data-saved-only]) shows saved ones.
  • Filters are kept in the URL as ?length= and ?level= (validated with own-key checks against WALK_LENGTHS and WALK_LEVELS), so a filtered list can be shared. dogs.html links to walks.html?level=easy.
  • "Show on map" ([data-show-route], aria-pressed) draws that route in ox-blood on the map; routes of filtered-out walks fade. [data-map-label] names the shown walk.
  • "Save" ([data-walk-save], aria-pressed) adds or removes a walk; the count shows in [data-saved-count].
  • [data-walk-count] reads "Showing 3 walks of 8."; [data-walk-empty] offers "Show all walks".

Events#

initEvents() builds the list for the next EVENT_WINDOW_DAYS days from EVENT_RULES and EVENTS, sorted by date and time in London. Today's events stay listed until three hours after they start and are labelled "Today" or "Tonight" (from 5pm); tomorrow's say "Tomorrow". The type filter ([data-event-type]) is kept in the URL as ?type=quiz|music|food|special. On the home page [data-next-event] shows the next event.

Demo dates: while KEEP_DEMO_CURRENT is true, a one-off event whose date has passed is moved on by whole years until it is today or later, so the lantern walk is always on 31 October and Burns Night on 25 January. Events with sameWeekday: true (the wreath workshop, carols, wassail and beer festival) move to the nearest day with the same weekday instead, so a Saturday event stays on a Saturday. Set KEEP_DEMO_CURRENT = false when you enter real dates.

Rules in the demo: The Hart Quiz every Tuesday at 8:30pm, the folk session on the first and third Thursday at 8pm, supper club on the last Friday at 7pm.

Gift vouchers#

initVouchers() on vouchers.html updates the preview ticket as you type: the voucher title (£50 or the experience name), recipient, sender and message, the message counter ("150 characters left", "1 character too many") and the total (voucher plus £3 postage for a printed card). Choosing "Printed card by post" swaps the recipient email field for a postal address. Money vouchers must be a whole number of pounds from £10 to £500.

Contact topic#

contact.html?topic=rooms preselects the topic (general, rooms, table, events, vouchers, dogs; unknown values are ignored). Other pages link to it with ?topic=table, ?topic=events and ?topic=dogs.

Header, mobile menu, scroll reveals and back to top#

The menu button toggles #mobile-menu with aria-expanded; Esc closes it and returns focus. Elements with reveal fade in as they scroll into view (everything above a revealed element is revealed too); with reduced motion they show at once. A back-to-top button (created in initBackToTop(), styled by .back-to-top) appears after scrolling one screen and moves focus to #main.

Forms#

All forms validate in the browser with messages under each field and aria-invalid, then show a success panel that says clearly that nothing was sent. They are marked with data-validate, and data-success names the success panel.

Form Page Validates
#stay-check rooms.html Dates, Saturday minimum, guests and dogs for the room (see above)
#quick-check index.html Arrival and departure dates
#enquiry rooms.html Name, email, optional phone format, message length
#voucher-form vouchers.html Amount (money voucher), to, from, message length, recipient email or postal address, your email
#contact-form contact.html Name, email, message (10 to 2000 characters)
#newsletter every page Email

To send them for real, use a form service such as Formspree. Give the form an action, then in assets/js/main.js find the block marked Demo only: nothing is sent inside initForms() and replace it with a request:

js
// ---- Demo only: nothing is sent. Replace this block with a fetch() to your form service. ----
fetch('https://formspree.io/f/your-id', {
  method: 'POST',
  body: new FormData(form),
  headers: { Accept: 'application/json' }
}).then(function (res) {
  if (!res.ok) throw new Error('Send failed');
  success.hidden = false;
  form.hidden = true;
  success.focus();
}).catch(function () {
  announce('Sorry, that didn\'t send. Please ring us instead.');
});

Change the success panel text in the HTML to match. The voucher form takes no payment; connect it to your payment provider's checkout instead of a form service.

Saved data (localStorage)#

Key Shape Validated on read
hearth:enquiry { checkin, checkout, guests, dogs, room, name, email, phone, message } (every field optional) Dates must be real YYYY-MM-DD dates, arrival not in the past, departure after arrival; guests an integer 1 to 4; dogs 0 to 2; room an own key of ROOMS; strings trimmed to 120 characters (message 1000). Anything else is dropped.
hearth:saved-walks ["cove", "tarn"] Only ids that are own keys of WALKS, without duplicates

URL parameters override the saved enquiry for that visit. All storage goes through one safeStorage object: it is localStorage, or an in-memory stand-in when the browser blocks storage (a sandboxed iframe, strict privacy settings), so saving still works for the rest of the page view. To reset, use "Clear saved details" on the rooms page, unsave the walks, or run localStorage.removeItem('hearth:enquiry') in the browser console.

SEO and sharing#

Every page has its own <title>, meta description, theme-color, Open Graph tags (og:title, og:description, og:image, og:locale en_GB) and a Twitter card, in its <head>. og:image is the relative path assets/img/og-image.png; social sites need an absolute URL, so after deploying change it to e.g. https://www.example.co.uk/assets/img/og-image.png on every page. To regenerate the PNG after editing the SVG, open og-image.svg in a browser at 1200 x 630 and take a screenshot, or use any SVG-to-PNG converter.

Add a sitemap.xml listing the eight pages and a robots.txt that points to it. For richer search results you can add LodgingBusiness or BarOrPub structured data (JSON-LD) to index.html with your address, phone, opening hours and price range.

Accessibility#

  • Skip link, landmarks (header, nav, main, footer), one <h1> per page and logical headings.
  • Visible focus outlines (ox-blood on paper, amber on green), AA colour contrast throughout; the ember colour is used only on dark backgrounds or for decoration.
  • ARIA patterns: tabs on the food page, toggle buttons with aria-pressed for filters, saves and the map, aria-expanded on the menu button, aria-current="page" in the nav and aria-current="date" on today's hours.
  • Polite live regions for results, counts and announcements (#live-region); errors linked with aria-describedby.
  • Inline SVG maps have a <title> and <desc>; decorative SVGs are aria-hidden.
  • prefers-reduced-motion turns off reveals, the ember pulse and smooth scrolling.

When editing, keep one <h1>, label every new input, and make sure every aria-controls, aria-labelledby and aria-describedby points at an id on the page.

Building and deploying#

Rebuilding the CSS#

assets/css/style.css is compiled from src/input.css by Tailwind CSS v4, which scans the HTML files and assets/js/ for class names. With Node.js 24:

sh
npm install
npm run build    # once
npm run watch    # rebuild on every change

Classes that only appear inside JavaScript strings must be written out in full in assets/js/main.js so Tailwind finds them.

Deploying#

The folder is a static site. Upload it as-is to any static host: Netlify Drop (drag the folder in), GitHub Pages, Cloudflare Pages or plain web hosting. Configure your host to serve 404.html for missing pages. You don't need to upload src/, package.json or the Markdown files.

Troubleshooting#

  • The status says the fire's lit when the inn is shut. Check HOURS in assets/js/main.js. The status uses London time on purpose; change TIME_ZONE if your inn is elsewhere.
  • A room never shows as available. The demo isBooked() marks a fixed pattern of nights as booked. Replace it with your real availability, or return false to show every room as free.
  • Prices in the checker don't match the room cards. Prices live in both ROOMS (JavaScript) and the tables in rooms.html; update both.
  • New Tailwind classes have no effect. Rebuild the CSS (npm run build). Classes built from pieces in JS ('bg-' + colour) aren't found; write them in full.
  • Saved walks or the enquiry disappear after closing the tab. The browser is blocking storage (private mode, a sandboxed iframe); the site falls back to memory for that page view.
  • The specials board or events show the wrong day. They use London time, so a visitor in another time zone can see tomorrow's board late at night. That's intended; change TIME_ZONE to suit.
  • The social preview has no image. Make og:image an absolute URL after deploying.

License#

MIT. The code and all included artwork are free for personal and commercial use; the full text is in LICENSE.txt, included in the download. Zilla Slab, Alegreya and Caveat Brush are licensed under the SIL Open Font License. The Fallow Hart, its staff, guests and events are fictional.

Search documentation

Search the guides and every template's documentation