Overview#
Chapter is a literary website template for a novelist. The demo author is Clemency Thorne, a fictional
crime and historical novelist who lives in Canterbury, with four Esme Lark mysteries set on Romney Marsh,
two standalone novels and a new book, The Glass Marsh, on the way. The design borrows from a well-set book
jacket: a book-face serif, generous margins, cream paper with a faint grain, fleuron ornaments and a jacket
drawn in SVG for every book. It is built with plain HTML, Tailwind CSS v4 and one vanilla JavaScript file,
with no build step needed to run it. This is version 1.0.0 (see template.json and
CHANGELOG.md). Live demo: freeframe.dev/templates/chapter.
Copy uses British English, prices are in pounds (GBP) and every date and time is UK time
(Europe/London, GMT in winter and BST in summer).
Quick start#
- Unzip the download.
- Open
index.htmlin a browser. Everything works from the file system. - Or serve the folder, which is closer to how it will run online:
npx serve .You only need Node.js 24 or newer if you want to change the styles and rebuild the CSS (see Building and deploying).
Pages#
| File | Purpose | Notable sections and features |
|---|---|---|
index.html |
Home | New-release hero with jacket, countdown to publication day (or "out now"), pre-order links, early praise, the opening lines, "continue reading" bookmark, the series in reading order, author introduction, next three events, newsletter sign-up |
books.html |
The backlist | Series/standalone filter with counts (?show=), reading order or newest first (?order=), a "reading: page N of M" badge for books you have started |
book.html |
One book (?id=) |
Jacket, blurb, countdown when not yet published, buy links, formats with ISBNs and prices, reviews, first-chapter reader, the rest of the series, book club link |
events.html |
Events | Upcoming events in UK time, in person/online filter (?format=), visitor's own time for online events, .ics downloads, invitation information |
about.html |
About Clemency | Biography, SVG portrait, map of the places in the books, timeline, questions people ask |
book-clubs.html |
Book club guides (?book=) |
Book picker, themes, discussion questions, .txt and printable .svg guide downloads, copy button, hosting tips |
press.html |
Press and media kit | Short and long biographies with copy buttons, new-book details, generated press release and fact sheet, portrait and jacket downloads, contacts with copy-email buttons |
newsletter.html |
The Marsh Letter | Sign-up with region and interests, what subscribers get, excerpts from recent letters |
contact.html |
Contact (?topic=) |
Topic routing to agent, publicity, rights, events or Clemency, with fields per topic and a character counter |
404.html |
Not found | A "torn-out page" with links back |
Project structure#
chapter/
index.html, books.html, book.html, events.html, about.html,
book-clubs.html, press.html, newsletter.html, contact.html, 404.html
assets/
css/style.css compiled Tailwind CSS (generated, do not edit by hand)
js/main.js every script: data, jackets, countdown, reader, filters, downloads, forms
img/clemency.svg illustrated author portrait (400 x 480)
img/favicon.svg browser tab icon
img/og-image.svg social sharing image source (1200 x 630)
img/og-image.png social sharing image used by og:image
src/input.css Tailwind source: design tokens and components
package.json scripts to rebuild the CSS
README.md short introduction
DOCS.md this guide
CHANGELOG.md version history
LICENSE.txt license (added to the download)Customising#
Brand and colours#
Design tokens live in the @theme block at the top of src/input.css.
| Token | Default | Used for |
|---|---|---|
--color-paper |
#f5efe3 |
Page background (with a grain texture on body) |
--color-paper-2 |
#ece3d1 |
Deeper paper for panels |
--color-card |
#fbf8f1 |
Cards, forms and the reader page |
--color-ink |
#221c18 |
Text and the footer |
--color-muted |
#5b5047 |
Secondary text |
--color-rule |
#cbbca2 |
Hairlines and borders |
--color-oxblood, --color-oxblood-deep |
#7b2230, #5c1622 |
Links, buttons, drop caps, newsletter band |
--color-gilt |
#b08a3e |
Ornaments only (not text) |
--color-gilt-ink |
#7a5a1c |
Gilt-coloured labels on paper (AA contrast) |
--color-marsh, --color-marsh-2, --color-glass |
greens | The home page hero (the new book's colours) |
To change the accent from oxblood to, say, navy:
@theme {
--color-oxblood: #1f3a5f;
--color-oxblood-deep: #142842;
}The paper grain is a small SVG noise texture in the body rule of src/input.css; delete the
background-image line for flat paper. Useful component classes: .btn with .btn-solid, .btn-line,
.btn-glass, .btn-ghost-light and .btn-sm; .caps and .caps-sm (spaced capitals); .h-title,
.h-section and .lead; .ornament (a fleuron between hairlines); .dropcap; .link; .jacket
(spine shading and shadow around a jacket SVG); .chip; .label, .input, .check, .field-error.
Sections with a dark background use .on-dark, which switches link, muted-text, error and focus colours.
Typography#
Three Google Fonts are loaded in the <head> of every page:
- Libre Baskerville for headings, quotes and jacket titles (
font-display, token--font-display) - Crimson Pro for body text and the reader (
font-serif, token--font-serif) - Josefin Sans for spaced capitals, buttons and labels (
.caps, token--font-caps)
To swap one, change the <link href="https://fonts.googleapis.com/css2?..."> in each page and the matching
token in @theme, then rebuild the CSS. The jackets drawn in main.js and og-image.svg name the fonts
in their font-family attributes too.
Content#
Static text lives in the HTML files. Everything that repeats or is computed lives in data constants at
the top of assets/js/main.js:
| Constant | What it holds |
|---|---|
SITE_NAME, SITE_URL |
Author name and the site address used in downloads and calendar files |
TIMEZONE, LOCALE, CURRENCY |
'Europe/London', 'en-GB', 'GBP' |
STORAGE_KEYS |
{ reader: 'chapter-reader', size: 'chapter-reader-size' } |
NEW_RELEASE |
Id of the book the home page, announcement bar and book.html without ?id= are about ('the-glass-marsh') |
KEEP_DEMO_CURRENT, DEMO_ANCHOR |
true and '2026-10-17': move the new book's publication day and every event forward in whole weeks (see Demo dates) |
RETAILERS |
Bookshops: name, a short note and the link prefix (url + book id) |
FORMAT_NAMES |
hardback, paperback, ebook, audio |
SERIES |
Series names and jacket label (lark) |
BOOKS |
Every book: jacket, blurb, formats, reviews, first chapter, themes and questions |
EVENTS |
Events with date, times, venue, price and booking link |
EVENT_FORMATS |
Values accepted by events.html?format= |
CONTACT_TOPICS |
Values accepted by contact.html?topic= and who each topic goes to (must match the <option> values) |
A book looks like this (shortened):
{
id: 'salt-and-shingle', title: 'Salt and Shingle', lines: ['Salt and', 'Shingle'], series: 'lark', no: 2, pub: '2020-05-07',
imprint: 'Finch & Holloway', genre: 'Crime', setting: 'Hythe and the Royal Military Canal',
jacket: { motif: 'shingle', bg: '#8e3a24', fg: '#f7ecdc', accent: '#f0c9a1', line: '#e2b878' },
tagline: 'Every stone on the beach has a story. One of them is a lie.',
blurb: ['First paragraph.', 'Second paragraph.'],
formats: [
{ type: 'paperback', isbn: '978-1-0000-1221-7', pages: 352, price: 8.99, retailers: ['spine-and-story', 'bookhound', 'pagecraft'] },
{ type: 'audio', isbn: '978-1-0000-1227-9', length: '9 h 56 min', narrator: 'Philippa Strand', price: 17.99, retailers: ['listen-lane'] }
],
reviews: [{ quote: 'Sun, sea and slow dread.', source: 'Dead Letter Quarterly' }],
extract: { chapter: 'Chapter One', heading: 'Bunting', pages: [['Paragraph', 'Paragraph'], ['Paragraph']] },
themes: ['Family secrets'],
questions: ['How does the summer setting change the feel of the series?']
}seriesis a key ofSERIES, ornullfor a standalone;nois the place in the reading order.pubis the UK publication day. A book whose day hasn't come yet shows "Pre-order", the countdown and "Out <date>"; from midnight UK time on that day it shows "Buy" and its year.linesis how the title breaks on the jacket. A line that is justTheis set small and italic.extract.pagesholds the first chapter, one array of paragraphs per reader page. A paragraph of'* * *'is drawn as a section break.- The ISBNs in the demo are fictional (prefix
978-1-0000). Replace them with your own.
Link to a book with book.html?id=<id>; add #read to open the reader straight away.
An event:
{ id: 'winter-words', title: 'Winter Words', format: 'in-person', kind: 'Festival talk', venue: 'Lewes Town Hall',
town: 'Lewes', date: '2026-12-05', start: '15:00', end: '16:00', price: 9, book: 'the-glass-marsh',
url: 'https://example.com/tickets', blurb: 'A winter book festival talk and reading.' }date, start and end are wall-clock times in TIMEZONE. price: 0 shows "Free, booking needed".
launch: true adds a "Launch night" badge. An event disappears once its end time has passed.
Demo dates#
A template can wait a long time between downloads, so nothing dated is allowed to go stale. While
KEEP_DEMO_CURRENT is true, every event and every book whose pub is after DEMO_ANCHOR moves forward
by the number of whole weeks since DEMO_ANCHOR. Weekdays and times never change, so publication day is
always a Thursday about three to four weeks away and the launch party is always on publication night.
var KEEP_DEMO_CURRENT = true; // false once BOOKS and EVENTS have your real dates
var DEMO_ANCHOR = '2026-10-17'; // the week the demo dates were written forSet KEEP_DEMO_CURRENT = false when you enter real dates. The countdown then ends at midnight UK
time on publication day, the site switches to "out now" by itself (hero, announcement bar, buy links and
book page) and past events disappear.
What the demo shows at a few fixed clocks (useful for tests):
| Clock (UTC) | KEEP_DEMO_CURRENT |
Publication day and countdown | Events |
|---|---|---|---|
| 2026-10-17 10:00 | true |
Thursday 12 November, 25 days 14 hours 00 minutes | 9 upcoming (6 in person, 3 online); first: The Reading Room, live, Monday 19 October, 19:00–20:00 BST |
| 2026-10-23 12:00 | true |
Thursday 12 November, 19 days 12 hours | 8 upcoming; the 19 October event is over and hidden; first: Whitstable Library, Saturday 24 October, 14:00–15:15 BST |
| 2026-10-26 12:00 | true |
Thursday 19 November, 23 days 12 hours | 9 upcoming; first: Monday 26 October, 19:00–20:00 GMT |
| 2026-11-12 09:00 | false |
"Out now"; buy links say "Buy" | 7 upcoming; first: the launch party, 18:30–20:30 GMT |
Images and artwork#
There are no photos. Everything is SVG:
- Jackets are drawn by
jacketSVG(book, opts)inmain.jsfrom each book'sjacketobject:bg,fg(title and author),accent(motif),line(frame and label) andmotif, one of the keys ofMOTIFS:church,shingle,compass,lighthouse,bells,reeds. Any element withdata-jacket="<book id>"is filled with that book's jacket. To use real cover images, replace thejacketSVG()calls (or the[data-jacket]elements) with<img>tags at a 300:460 ratio and keep the.jacketclass for the spine shading. assets/img/clemency.svgis the portrait (400 x 480) used on the home, about and press pages. Replace it with a 5:6 photo and keep a meaningfulalttext.- The map on
about.htmlis inline SVG; edit the place names and dots there. - The social image is
assets/img/og-image.png(1200 x 630), made fromog-image.svg. Social sites don't show SVG, so export a new PNG after editing the SVG. assets/img/favicon.svgis the browser tab icon.
Navigation, header and footer#
The announcement bar, header and footer are repeated in every HTML file. To add a page, copy an existing
page, change its <title>, description, data-page on <body> and content, then add a link to the
header (nav[aria-label="Main"] on the left of the name, nav[aria-label="More"] on the right), the
mobile menu (#mobile-menu) and the footer in every file. Mark the current page with
aria-current="page" on its links.
Features#
Release countdown and "out now"#
fillRelease() in main.js fills every [data-release-date] with the new book's publication day, shows
[data-release-state="soon"] or [data-release-state="out"], sets [data-buy-verb] to "Pre-order" or
"Buy", fills [data-buy-links] with links to each bookshop that sells the book, and starts every
[data-countdown] (days, hours, minutes and seconds in [data-unit] boxes, with singular and plural
unit labels). When the countdown reaches zero the page switches to the "out" state without a reload.
The countdown has role="timer" and a hidden sentence ([data-countdown-label]) for screen readers.
Books filter and reading order#
books.html lists BOOKS with filter buttons (data-filter="all|lark|standalone", shareable as
books.html?show=lark) and order buttons (data-order="reading|published", ?order=published). In
reading order, series books are numbered and standalones follow; "Newest first" sorts by pub. The buttons
use aria-pressed, and the result line ([data-book-count]) is announced politely. Unknown parameter
values fall back to "all" and "reading".
Book pages#
book.html reads ?id=; an unknown id shows the new book with a short notice. Elements with
data-b="title|series|tagline|genre|setting|imprint|pub|verb" and the [data-b-*] containers are filled
from BOOKS. The table of formats sits in a scrolling wrapper on narrow screens.
First-chapter reader#
The reader on book.html ([data-reader]) shows extract.pages one page at a time:
- Start reading opens it (
aria-expandedon the button); Close the reader puts it away. - Previous/next buttons, or the Left and Right arrow keys while focus is inside the reader.
- Three text sizes (
data-reader-size="sm|md|lg",aria-pressed), remembered inchapter-reader-size. - Your place is saved after every page turn. Closing and reopening, reloading or coming back later resumes on the same page ("Continue from page 3"), and the home page shows a "continue reading" bookmark for the last book you read. The books page shows a "Reading: page N of M" badge.
- After the last page, an end card offers the buy links and Start again, which clears the place.
- The status line (
[data-reader-saved]) is a polite live region; page turns respect reduced motion.
Events#
upcomingEvents() keeps events whose end time is still in the future and sorts them. events.html filters
them with data-format="all|in-person|online" (?format=online); counts and the result line use the
right singular or plural ("1 upcoming event"). For online events, visitors in another time zone also see
their own time. Add to calendar downloads an .ics file (UTC times) built by icsText(). The home
page shows the next three ([data-next-events]), or a newsletter prompt when there are none.
Book club guides#
book-clubs.html fills its <select> from BOOKS and shows the chosen book's themes and questions
(?book=<id>). Download the guide saves a plain-text file and Printable page an A4-sized SVG
with the jacket and numbered questions (guideText() and guideSVG()). Copy the questions copies the
list.
Press kit, copy and downloads#
Any <button data-copy="#element-id"> copies that element's text; data-copy-text="..." copies a fixed
string (the contact emails). The button label changes to "Copied" for a moment and a toast confirms.
data-download="press-release" builds a press release from the new book's data and the short biography;
data-download="factsheet" saves every [data-fact] row; data-jacket-download="<id>" saves a jacket as
a standalone SVG. Downloads are made in the browser with Blob, so nothing needs uploading.
Scroll reveals, menu and back to top#
Elements with .reveal fade in once (and everything above them, so fast jumps never leave gaps). The
mobile menu button has aria-expanded, Escape closes it and returns focus. A back-to-top button appears
after scrolling. All motion is off with prefers-reduced-motion: reduce.
Forms#
Every form validates in the browser and shows inline messages under each field, then a success state.
The demo forms don't send anything. Rules come from attributes: required, type="email" and
data-minlength. Fields in hidden contact topics are disabled, so they are skipped.
| Form | Where | Validates |
|---|---|---|
Newsletter ([data-newsletter-form]) |
index.html, newsletter.html |
Email, consent checkbox |
Contact ([data-contact-form]) |
contact.html |
Topic, name, email, message (20 characters or more) and the chosen topic's required fields |
The contact form preselects its topic from contact.html?topic=agent|publicity|rights|events|readers
and shows who the message goes to (from CONTACT_TOPICS).
To connect a form to a service such as Formspree, replace the comment in the form's submit handler in
initForms() or initContact():
fetch('https://formspree.io/f/your-id', {
method: 'POST',
body: new FormData(form),
headers: { Accept: 'application/json' }
}).then(function (res) {
if (res.ok) showSuccess(form, '');
});For the newsletter, use your mailing list's form action (Buttondown, Mailchimp, ConvertKit and others accept a plain POST) and keep the consent checkbox.
Saved data (localStorage)#
| Key | Shape | Validation |
|---|---|---|
chapter-reader |
{ "last": "the-glass-marsh", "pages": { "the-glass-marsh": 3 } } |
Only ids that exist in BOOKS (own-key check) and whole page numbers from 1 to that book's page count are kept; last must be one of them |
chapter-reader-size |
"sm", "md" or "lg" |
Must be a key of READER_SIZES, otherwise "md" |
Storage goes through one safeStorage object. When localStorage is blocked (a sandboxed iframe, strict
privacy settings), it falls back to memory, so the reader still remembers your place for the page view.
To reset, run localStorage.removeItem('chapter-reader') in the browser console or clear the site's data.
URL parameters are validated the same way, with own-key checks against BOOK_BY_ID, BOOK_FILTERS,
BOOK_ORDERS, EVENT_FORMATS and CONTACT_TOPICS:
| Page | Parameter | Values |
|---|---|---|
book.html |
id |
A book id (default NEW_RELEASE); #read opens the reader |
books.html |
show, order |
all, lark, standalone; reading, published |
events.html |
format |
all, in-person, online |
book-clubs.html |
book |
A book id |
contact.html |
topic |
agent, publicity, rights, events, readers |
SEO and sharing#
Each page has its own <title>, meta description and Open Graph tags in the <head>; book.html also
sets document.title to the chosen book. og:image points to assets/img/og-image.png with a relative
path; most social sites need an absolute URL, so change it to https://your-domain/assets/img/og-image.png
once you know the address. Add a sitemap.xml listing the pages, and consider Book structured data
(JSON-LD with name, author, isbn and datePublished) on book pages.
Accessibility#
- Skip link, landmarks, one
<h1>per page, current page marked witharia-current="page". - Visible focus outlines in oxblood (glass green on dark sections).
- Filter, order and text-size buttons use
aria-pressed; the reader and menu buttons usearia-expandedandaria-controls. - Jackets have text alternatives where they carry meaning and are hidden where the title is already next to them.
- Live regions announce result counts, reader progress, copy confirmations and the contact routing.
- Text colours meet WCAG AA on every paper tone; gilt is used as text only in the darker
gilt-ink. - Motion (reveals, page turns, smooth scrolling) is off with reduced motion.
When editing, keep one <h1> per page, give new inputs a <label>, and keep aria-controls and
aria-describedby pointing at ids that exist.
Building and deploying#
The compiled CSS is committed, so the site runs as it is. After changing src/input.css or adding new
Tailwind classes to the HTML or main.js, rebuild it (Node.js 24):
npm install
npm run build # once
npm run watch # rebuild on every changeUpload the folder to any static host: Netlify Drop, Cloudflare Pages, GitHub Pages, or any web server. All links are relative, so it also works in a subfolder.
Troubleshooting#
- The publication day or event dates are wrong.
KEEP_DEMO_CURRENTis stilltrue, so dates are moved forward. Set it tofalseonceBOOKSandEVENTShave your real dates. - The site still says "Pre-order" after publication. Check the book's
pubdate and thatNEW_RELEASEis its id. The switch happens at midnight UK time. - No events are listed. With
KEEP_DEMO_CURRENT = false, every event's end time has passed. Add new ones toEVENTS. - The reader opens on the wrong page. It resumes where you stopped. Press Start again at the end of
the chapter, or clear
chapter-reader(see Saved data). - A new Tailwind class has no effect. Rebuild the CSS. Classes written only inside
main.jsstrings must appear there literally to be generated. - Downloads or copying don't work in a preview. Some sandboxed previews block downloads and the clipboard. Open the page directly in a browser.
License#
MIT. The code and all included artwork are free for personal and commercial use; see LICENSE.txt.
Libre Baskerville, Crimson Pro and Josefin Sans are licensed under the SIL Open Font License.
Clemency Thorne, her books, publisher, agency, bookshops, reviewers and events are fictional.