Overview#
Notebook is a personal developer blog as a WordPress block theme. The demo belongs to Imke Vos, a fictional backend engineer in Rotterdam who writes about databases, build tools and debugging. Posts are WordPress posts: a post's series is its category (posts that stand alone are in Notes) and its tags are WordPress tags. The front page shows the newest post and the five before it; each post has a series badge, highlighted Code blocks with copy buttons, heading links, an "On this page" outline that follows your reading, a reading progress bar, a series box and links to the older and newer posts; the Archive, Tags and Search pages list, filter and search every post. Series, Projects, About (with a now section) and Subscribe pages complete the site, on graph paper with a red margin line and a yellow highlighter, in light and dark mode. This guide covers version 1.0.0.
Live demo: freeframe.dev/templates/notebook
How the WordPress version is organised#
| Part | Where | Edit it in |
|---|---|---|
| Pages (Home, Archive, Series, Tags, Projects, About, Search, Subscribe) | Real pages, created from page patterns by Notebook setup | Pages > Edit |
| Posts | WordPress posts; series are categories, tags are tags | Posts |
| Post layout | templates/single.html → patterns/post.php |
Appearance > Editor > Templates > Single Posts |
| Lists of posts | Query Loops (patterns/home-latest.php, patterns/posts.php, both using patterns/post-row.php) |
Editor, or the page |
| Archive, tags, search, series box, older/newer | Built in the browser by assets/js/main.js from a post index printed by inc/content.php |
Your posts (nothing to edit) |
| Header and footer | patterns/header.php, patterns/footer.php |
Appearance > Editor > Patterns |
| Colours, fonts, sizes | theme.json |
Appearance > Editor > Styles |
Sections are block patterns in the "Notebook" category, so each one can also be inserted into any page.
Quick start#
-
Appearance > Themes > Add New Theme > Upload Theme: upload the zip from freeframe.dev and activate Freeframe Notebook (WordPress 7.1 or later, PHP 7.4 or later).
-
Appearance > Notebook setup > Create the demo content. It creates:
- the categories Postgres from the inside, A build system in 200 lines, CSS for backend people (series) and Notes;
- fourteen sample posts with their tags, dates, series parts and reading times (one with full text, the others short placeholders);
- the pages Home, Archive, Series, Tags, Projects, About, Search and Subscribe, Home as the front page, and the Main and Mobile menus.
Anything that already exists (same slug) is left alone, so running it twice is harmless.
-
Delete the sample posts you don't want, and write your own.
To try it without a server: pnpm install && pnpm dev in the theme folder (Node.js 24 and pnpm) starts
WordPress in WordPress Playground at http://127.0.0.1:9400 with the theme and the demo content.
Pages and templates#
| Page or template | Pattern(s) | Notable features |
|---|---|---|
Home (/) |
home-intro, home-latest, home-series, home-tags, newsletter |
Newest post as a card, five latest posts, series cards, tags, newsletter form |
A post (templates/single.html) |
post |
Series badge, outline, progress, Code blocks with copy buttons, series box, older/newer posts |
Archive (/archive/) |
archive |
Every post grouped by year with jump links |
Tags (/tags/) |
tags |
Tag buttons with counts, ?tag= in the address |
Series (/series/) |
series-intro, series-postgres, series-build, series-css, series-next |
Each series with its parts in order |
Search (/search/) |
search |
Instant search with highlighted matches, ?q= in the address |
About (/about/) |
about-intro, about-now, about-uses, about-contact |
Portrait, now, uses, contact |
Projects (/projects/) |
projects, talks |
Project cards and talks |
Subscribe (/subscribe/) |
subscribe |
Email form, RSS address with a copy button |
404 (templates/404.html) |
404 |
Search box and links back |
Blog, archives, WordPress search (index.html, archive.html, search.html) |
posts |
Lists of posts in the same rows as the home page |
| Page with title (custom template) | none | For simple text pages you add |
Project structure#
freeframe-notebook/
style.css theme header (name, version, requirements)
theme.json colours (light and dark), fonts, font sizes, layout widths
functions.php loads inc/
inc/setup.php styles, script, dark-mode script, head tags, script strings, pattern categories
inc/content.php post URLs, post fields, block bindings, term links, the post index
inc/site-setup.php Appearance > Notebook setup and the demo content
inc/block-styles.php button styles (Outline, Series badge)
inc/icons.php the icon collection for the Icon block (assets/icons)
templates/ page, single, index, archive, search, 404, page-with-title
parts/ header and footer (each loads its pattern)
patterns/ sections, page patterns, the post layout and lists of posts
assets/css/ theme.css and editor.css (compiled, don't edit)
assets/js/main.js all interactive features
assets/fonts/ Martian Mono, Atkinson Hyperlegible Next and Mono (woff2 + licenses)
assets/img/ logo, favicon, illustrations, portrait, social image
assets/icons/ icons used by Icon blocks
src/theme.css Tailwind CSS source of assets/css/theme.css
languages/ translation template (freeframe-notebook.pot)
blueprint.json WordPress Playground setup used by pnpm devWriting posts#
Posts > Add New:
- Title and Excerpt (Post sidebar > Excerpt): the summary shown under the title and in every list. Lists and search use it, so write one for every post.
- Category: the series, or Notes for a post that stands alone. A new category is a new series (every category except Notes and the default "Uncategorized" counts as one); give it a description.
- Tags: lower-case tags read best, since they're shown as
#postgres. - Text: Heading 2 and Heading 3 blocks become the outline and get a
#link. Use Code blocks for code; Notebook gives each one a language label and a copy button.
Posts left in "Uncategorized" are not listed by the home page, the archive, tags or search; give them a series or Notes.
Code blocks#
Add the class lang-sql (or lang-bash, lang-js, lang-rust, lang-css...) in the Code block's
Advanced > Additional CSS class(es) to label its language; without one it reads "Output". The demo's
example post keeps hand-made highlighting as <span class="tok-k"> (keywords), tok-s (strings),
tok-n (numbers), tok-c (comments) and tok-f (functions) inside its Code blocks (edit them as HTML);
plain Code blocks show unhighlighted code in the code font.
Post fields#
Under Preferences > Panels > Custom Fields:
| Field | Meaning |
|---|---|
reading_time |
Minutes shown in lists, e.g. 6. Empty: counted from the text at 230 words a minute |
series_part |
The part number in the series, e.g. 2. Empty: counted from the dates of the series' posts |
On the post itself the reading time is counted from the text in the browser.
Customising#
Brand and colours#
Appearance > Editor > Styles > Colors edits the palette in theme.json. Every colour has a light and a
dark version: Paper / Paper (dark) is the background, Ink the text, Muted secondary text, Line borders,
Pen the red pen (margin lines, labels, series badges, active outline entry), Pen soft the series badge
background and Marker the highlighter (link underlines, the current menu item, search matches).
src/theme.css maps them to the variables the design uses (--paper, --ink, --pen, --marker...),
and also holds the code colours (--code-bg, --tok-k...) and the graph-paper --grid for each theme.
Typography#
Martian Mono (headings and labels, condensed with font-stretch: 87.5%), Atkinson Hyperlegible Next
(text) and Atkinson Hyperlegible Mono (code) are bundled in assets/fonts/ and declared in
theme.json, so no font is loaded from Google. Change them in Styles > Typography or in theme.json.
Dark mode#
The theme switch in the header stores "dark" or "light" under notebook-theme in the browser; a
small script printed first in <head> (inc/setup.php) applies it before the page paints, otherwise
the visitor's system setting is used. To turn dark mode off, remove the switch from the header pattern
and add add_filter( 'freeframe_notebook_dark_mode', '__return_false' ); to a child theme.
Navigation, header and footer#
Appearance > Notebook setup creates the Main menu and Mobile menu (Archive, Series, Tags,
Projects, About). Edit them in Appearance > Editor > Navigation. The search link (with its /
shortcut), the theme switch and the menu button are small Custom HTML blocks in the header pattern; the
footer columns are lists you can edit in Appearance > Editor > Patterns > Footer.
Content in pages#
Every page is ordinary blocks: edit the text, links, buttons and pictures in Pages > Edit. The series page's parts, the home page's series cards and tags and the projects are written in the pages (update them when you publish); lists of posts update themselves.
Classes the script reads#
Blocks can't keep data- attributes, so patterns write them as classes that assets/js/main.js turns
back into attributes before anything else runs: ffd-<name>--<value> becomes data-<name>="<value>",
ffa-<name>--<value> becomes an attribute such as aria-labelledby, and ff-hidden becomes hidden.
The post content has ffd-post-body (headings, code blocks and reading time), the reading time
paragraph ffd-reading-time and the progress bar ffd-progress. Keep these classes when you edit
those blocks.
Features#
Lists of posts#
The home page's newest post and latest posts, the blog page and the date archives are Query Loops
using patterns/post-row.php: date, title, excerpt, the series badge and the reading time (both from
the freeframe-notebook/post block bindings source in inc/content.php) and the tags. Tag links open
the Tags page (/tags/?tag=postgres) and series links the Series page (/series/#<slug>).
The post index#
On every page inc/content.php prints window.NOTEBOOK_POSTS (slug, title, excerpt, date, tags,
series, part, minutes and address of every post outside the default category, newest first),
window.NOTEBOOK_SERIES and window.NOTEBOOK_LINKS before the script, and on a post
window.NOTEBOOK_CURRENT. Filter freeframe_notebook_index to change it.
The post page#
patterns/post.php: the series badge (a Button block with the "Series badge" style), title, excerpt,
date, reading time, a Copy link button and the tags, then the series box, the outline, the text,
the older/newer links and a note about replies. In the text, every Code block gets a frame with its
language and a Copy button (it reads "Copied" for two seconds and a message is announced), every
Heading 2 and 3 gets an id and a # link that also copies the section's address, and the outline (a
sidebar on wide screens, "On this page" on phones) highlights the section you are reading. The bar at
the top shows how far you have read.
Archive, tags and search#
The Archive page groups every post by year with links to each year. The Tags page shows a button for
each tag (most used first), lists the posts of the chosen one, announces the count and keeps the tag in
the address; ?tag= is matched in any case and a tag that doesn't exist is ignored with a short note.
The Search page searches titles, excerpts, tags and series as you type (every word must match), marks
the matches and keeps the query in ?q=. Press / on any page to go to search. The theme stops
WordPress from reading /tags/?tag= as a tag archive (inc/content.php).
Scroll reveals, mobile menu, back to top#
Elements with the class reveal fade in as they scroll into view (not with reduced motion). The mobile
menu opens below the header, closes with Escape and returns focus to its button. A back-to-top button
appears after about one screen.
Forms#
| Form | Pattern | Fields |
|---|---|---|
| Newsletter | newsletter (home) |
|
| Email subscription | subscribe |
first name (optional), email, how often, series announcements |
| Search | search, 404 |
query (a plain GET form to the Search page) |
Forms with data-validate are checked in the browser: errors appear under the field, fields get
aria-invalid, and the first invalid field is focused. When everything is valid the form resets and
its [data-form-success] message is shown and focused.
Nothing is sent anywhere: they are demo forms until you connect them, with a newsletter plugin
(replace the form's Custom HTML block with the plugin's block; add the classes input, label and
btn to its fields and button) or a service such as Buttondown or Formspree: add its endpoint as the
form's action and method="post", and in assets/js/main.js, at the comment "Send the data to your
form endpoint here", send it with fetch(form.action, { method: 'POST', body: new FormData(form), headers: { Accept: 'application/json' } }) (in a child theme's copy of the script). Also rewrite the
thank-you texts, which say that nothing was sent.
Saved data (localStorage)#
| Key | Value | Purpose |
|---|---|---|
notebook-theme |
"dark" or "light" |
The reader's theme choice; any other value is ignored and the system setting is used |
SEO and sharing#
Page titles come from WordPress: the page or post title, an em dash and the site name; the front page
shows the site name and tagline. The theme doesn't output meta descriptions or Open Graph tags; use an
SEO plugin (Yoast SEO, Rank Math, The SEO Framework) and give it assets/img/og-image.png as the default
social image. WordPress generates a sitemap at /wp-sitemap.xml and an RSS feed at /feed/; put that
address in the Subscribe page's feed box.
Accessibility#
Built in: a skip link (added by WordPress, styled by the theme), landmarks (header, labelled navigation,
main, labelled sections and asides, footer), one h1 per page, visible focus outlines, the theme switch
and tag buttons with aria-pressed, the mobile menu button with aria-expanded and Escape, live
regions for counts, copy messages and form messages, focusable and labelled code blocks, heading links
named after their section, aria-current on the current menu item, outline entry and series part, and
reduced-motion support.
When you edit: keep one Heading 1 per page and use Heading 2 and 3 for a post's sections, give pictures alt text, and check the contrast of any colour you change in both themes (WCAG AA).
Building and deploying#
You only need this if you change the theme's files. 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/:
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)
pnpm dev # build, then run WordPress Playground with the theme at http://127.0.0.1:9400To install the theme, zip the folder (without node_modules/) and upload it in Appearance > Themes.
Translations#
Every text in the patterns, the setup and the script is translatable with the text domain
freeframe-notebook; languages/freeframe-notebook.pot is the template. Put
freeframe-notebook-<locale>.mo in the theme's languages/ folder or in
wp-content/languages/themes/. The script's strings are translated in inc/setup.php
(freeframe_notebook_script_strings()).
Child themes#
Create wp-content/themes/notebook-child/style.css with Template: freeframe-notebook, then override
templates or patterns by copying them into the child theme. Filters: freeframe_notebook_dark_mode,
freeframe_notebook_demo_posts (before the setup creates posts) and freeframe_notebook_index (the
post index).
Troubleshooting#
| Problem | Fix |
|---|---|
| A post is missing from the home page, archive, tags or search | It is in "Uncategorized"; give it a series or Notes |
| The series badge shows on a post that isn't in a series | Its category is a series; move it to Notes |
| A Code block has no language label | Add the class lang-<name> in Advanced > Additional CSS class(es) |
/tags/?tag=… shows every post |
The tag has no posts, or the Tags page's slug isn't tags |
| Tag links go to WordPress tag archives | The theme sends them to the page with the slug tags; create it with Notebook setup |
| New classes have no effect | Run pnpm build so Tailwind generates them |
License#
MIT (GPL-compatible), see LICENSE.txt in the download. The illustrations, portrait, logo and icons are
original artwork included under the same license. The fonts are under the SIL Open Font License 1.1
(see assets/fonts/*/OFL.txt).