14 September 2026 · Sam Whitlock

Five small code tips from the club’s website

Our reading list page is a few dozen lines of code. Here is what we learned writing it, with every snippet ready to paste and run.

Several of you have asked how the What we are reading page works, and whether it is hard to build one for your own blog. It is not. The whole thing is a short list, a couple of helper functions and a stylesheet, and most of what we learned along the way fits in a paragraph each.

So here are five small tips, in the order we needed them. Every snippet runs as shown. If you have never written a line of code, skim the headings and read the tip boxes: they are the part we wish somebody had told us first.

test ABC 123

1. Keep the reading list as data

Our first version had the books written into the page, one paragraph each. It lasted until the second month, when somebody wanted them sorted by author. The fix was to separate the facts from the page: a list of plain objects, and a few functions that read it.

src/data/reading-list.ts
export type Book = {
  title: string;
  author: string;
  pages: number;
  /** The month the club read it, as "2026-06". */
  month: string;
};

export const readingList: Book[] = [
  { title: 'Hamnet', author: 'Maggie O’Farrell', pages: 305, month: '2026-06' },
  { title: 'Jane Eyre', author: 'Charlotte Brontë', pages: 532, month: '2026-07' },
  { title: 'H is for Hawk', author: 'Helen Macdonald', pages: 300, month: '2026-08' },
];

export const totalPages = readingList.reduce((sum, book) => sum + book.pages, 0);

export function monthName(month: string): string {
  const [year, number] = month.split('-').map(Number) as [number, number];
  return new Intl.DateTimeFormat('en-GB', { month: 'long', year: 'numeric', timeZone: 'UTC' }).format(
    new Date(Date.UTC(year, number - 1, 1)),
  );
}

The highlighted lines are the whole data set and the one sum we show at the top of the page. monthName("2026-06") gives June 2026, in British spelling, with no library and no time zone surprises: the timeZone: 'UTC' option stops a book read on the first of the month from sliding back into the previous one.

Where should the list live?

A TypeScript file is only one answer. We tried four before settling.

Four places to keep a reading list, and what each one costs.
Where it livesWho can edit itWatch out for
A TypeScript fileWhoever can codeEvery change is a code change
A JSON fileAnyone with a text editorNo comments, no trailing commas
A CSV exportAnyone with a spreadsheetTitles with commas need quoting
A database tableThe site, through a formOne more thing to back up

Tip: write the boring version first

Start with the plain list in one file. You can move it to a spreadsheet or a database later, and because the page only ever reads readingList, nothing else has to change.

2. Let the shell do the small sums

Before you write a function to add up the pages, ask whether the terminal can. The club keeps a spreadsheet, and exporting it as a CSV file takes ten seconds. From there, awk does the adding. Everything below runs in any bash shell.

count-the-pages.sh
cat > books.csv <<'EOF'
title,pages
Hamnet,305
Jane Eyre,532
H is for Hawk,300
EOF

# Skip the header row (NR > 1) and add up the second column.
awk -F, 'NR > 1 { total += $2 } END { print total " pages" }' books.csv

It prints 1137 pages. The same trick works on the web. Open Library has a free search API, and one long line gets you a book’s title, author and typical page count. You need curl and jq installed, and the line is long on purpose: turn on Wrap in the block’s settings and it reads without a scrollbar.

look-up-a-book.sh
curl --silent --fail --get "https://openlibrary.org/search.json" --data-urlencode "title=The Remains of the Day" --data-urlencode "fields=title,author_name,number_of_pages_median" --data-urlencode "limit=1" | jq '.docs[0]'

3. Ask the database, do not loop

When the list grew past a few dozen books we moved it into a table, and the question which author have we read the most of? turned into one query instead of a loop and a counter. This works as written in SQLite and in Postgres.

most-read-authors.sql
CREATE TABLE books (
  title  TEXT    NOT NULL,
  author TEXT    NOT NULL,
  pages  INTEGER NOT NULL
);

INSERT INTO books (title, author, pages) VALUES
  ('Wolf Hall', 'Hilary Mantel', 672),
  ('Bring Up the Bodies', 'Hilary Mantel', 432),
  ('Hamnet', 'Maggie O’Farrell', 305);

SELECT author, COUNT(*) AS books, SUM(pages) AS pages
FROM books
GROUP BY author
HAVING COUNT(*) > 1
ORDER BY pages DESC;

The result is one row: Hilary Mantel, two books, 1104 pages. Add a fourth book by her and the answer updates itself.

4. Give long text a comfortable measure

The biggest improvement to the page was not code at all. It was one rule: lines of about 65 characters, with room between them. In CSS you can say that directly with the ch unit, and let the browser balance the headings.

styles/reading.css
.book-notes {
  max-inline-size: 65ch;
  margin-inline: auto;
  font-size: clamp(1.05rem, 1rem + 0.3vw, 1.2rem);
  line-height: 1.7;
  text-wrap: pretty;
}

.book-notes :is(h2, h3) {
  text-wrap: balance;
}

@media (prefers-reduced-motion: no-preference) {
  html {
    scroll-behavior: smooth;
  }
}

Tip: respect the reader’s settings

The last rule only smooths the scrolling for people who have not asked their device to reduce motion. It costs three lines and nobody has ever noticed it, which is how good defaults should feel.

5. Check your settings before you trust them

The club’s rules live in a small club.json file. JSON is easy to break with one stray comma, and the site would only tell you when a member visited. Make the failure loud and early: parse the file as part of your build, or by hand as below.

club.json
{
  "name": "The Playground Book Club",
  "meets": "first Thursday of the month",
  "shortlist": 4,
  "ballots": 2,
  "tieBreak": "shorter book"
}
check-club-json.sh
node -e "JSON.parse(require('node:fs').readFileSync('club.json', 'utf8')); console.log('club.json is valid JSON')"

If it prints club.json is valid JSON, you are done. If it points at a line and a column, that is the comma. Fix it, and run it again.

Want to build your own?

None of this needs a framework, and all of it can start on a train. Bring a laptop to the next meeting and we will build a reading list page together, just ask.