Skip to content

Releases: Dhanabhon/tome-cms

TomeCMS 1.5.0

Choose a tag to compare

@github-actions github-actions released this 30 Sep 06:48
Immutable release. Only release title and notes can be modified.

TomeCMS 1.5.0

Date: 2026-09-30
Status: Stable release; tagged and published as a GitHub release

TomeCMS 1.5.0 takes the operating system out of the admin: every list, date, time and colour is chosen
in the admin's own controls, and no form lets the browser draw its own warning. Save buttons now say
what they are doing, the File Manager takes several files at once into a folder you choose, and a post
can keep its cover off the top of the article. /api/v1 gains one field, showCover; nothing is
renamed or removed.

Upgrading

  • A managed install updates from "System" in the admin, as
    Updating describes. 1.5.0 brings one
    migration, 027_post_show_cover, so the updater takes a full backup (PostgreSQL and the bucket)
    before it, and the site is offline a little longer than for an update without one.
  • A build from source upgrades in place by running the deploy helper again, which runs the
    migration.

The migration adds posts.show_cover, a boolean not null default true: every existing post keeps
showing its cover. There is no new setting and no new runtime dependency.

deploy/cloud-init.yaml now installs 1.5.0.

No system pickers

The design is in the spec. In short:

  • Lists open the admin's own list everywhere, including the Posts and Pages language filters and
    the Redirects article list. A list that filters a page applies on a choice (a click or Enter), not
    while arrowing through it.
  • Dates and times have a picker of their own: a month grid in the owner's calendar, with hour and
    minute fields, Clear and Done. It is driven by the keyboard (arrows, Home/End, PageUp/PageDown,
    Enter), closes on Escape or a press outside, and keeps its time row and Done in view on a short
    phone screen. It serves a post's and a page's publish date, Maintenance's return time and a slide's
    start and end.
  • Colours in a plugin's settings are typed as hex beside a swatch.
  • Forms take noValidate: an empty name or address is told in the admin's words, beside the field,
    and the field takes the focus.
  • Radios and checkboxes use the accent colour instead of the browser's blue.

The file chooser behind "Upload" is the one exception: the web does not let a page replace it. A unit
test fails on a native <select>, date, time or colour input, or a form without noValidate, in the
admin.

The admin

  • Save buttons carry their state: a spinner while saving, "Saved" after, and "Save" again once
    something changes. The button keeps one width, and a hidden status line says the same words to a
    screen reader. The words beside the button are gone.
  • The row menu on Posts and Pages, and the File Manager's folder menu, float with a shadow, set
    Delete apart, close on a press outside or Escape, and one opens at a time.
  • A white main column. The screens sit on --color-paper; the sidebar stays on the cream page
    behind them.
  • The Plugins notes ("Where plugins come from", "A plugin cannot lock you out") sit under the plugin
    grid at their own height with 16px headings; the admin's accent bars are 2px.

The File Manager

Choosing files opens an upload dialog. Pick the folder (the one on screen by default), and the files go
up two at a time, each with its type, size and progress. A file the library cannot take is told at once
and never sent; a file that fails in transit has its own Retry, and the others carry on. Stop cancels
what is still running, and the library opens on the chosen folder with whatever landed. Choosing a file
from a post (a cover, an image) still takes one file, as before. The upload button loses its focus ring
after a file is chosen.

The cover in the article

A post's settings have "Show the cover at the top of the post", on by default. Turned off, the cover
leaves the top of the article only: the home page card and a shared link's og:image still use it.
Each language's edition has its own switch, and a translation or a duplicate starts with the source's
value. /api/v1 posts carry showCover.

The public themes

Paper's footer sets the TomeCMS credit as its own line at the right; on a phone it wraps under the
copyright at the left.

For theme authors

articleCover(post) in src/lib/post-cover.ts returns the cover to show at the top of an article (or
null) and the body to render; both bundled themes use it. A theme that reads cover_image directly
keeps working and shows the cover whatever the switch says.

Testing

  • tests/e2e/admin-150.spec.ts covers the save button's states, the row menu's closing, the date
    picker by keyboard, the multi-file upload with a refused file, a retry and Stop, and the cover switch
    with og:image.
  • Unit tests cover the calendar arithmetic, popover placement, the upload queue, hex colours, the save
    state, the row menu's closing rules and articleCover; tests/unit/admin-surface-tokens.test.ts
    guards against native pickers and forms without noValidate.
  • The migration runs in npm run test:operations:update, and npm run check:inventory lists it.

TomeCMS 1.4.0

Choose a tag to compare

@github-actions github-actions released this 29 Sep 17:51
Immutable release. Only release title and notes can be modified.

TomeCMS 1.4.0

Date: 2026-09-30
Status: Stable release; tagged and published as a GitHub release

TomeCMS 1.4.0 changes the shape of the admin. Every screen but the editor now reads as an editorial page
instead of a set of equal boxes, and on the public site the search box moves onto the row of categories.
Colours, fonts, screens and what each screen does are unchanged. /api/v1 is unchanged.

Upgrading

  • A managed install updates from "System" in the admin, as
    Updating describes. There is no
    migration, no new setting and no new runtime dependency, so an updater of 1.3.0 or later backs up the
    database alone before it.
  • A build from source upgrades in place by running the deploy helper again.

deploy/cloud-init.yaml now installs 1.4.0.

The admin

The design is in the spec. In short:

  • A masthead, not a top bar. From 64rem up there is no bar over the screen. Each screen opens with a
    small uppercase label naming its group, the title in the display face at 40px, a line about the
    screen, its actions on the title's baseline, and a rule. Phones keep a thin bar with the menu.
  • The sidebar sits on the page. It is no longer a white column. The site's name is a line under the
    logo, group names are small labels, counts are plain figures, and the screen you are on is ink with a
    2px green bar beside it rather than a filled row, since no two surfaces in this palette are far enough
    apart to show a state.
  • Sheets, not cards. The form screens (Settings, Maintenance, Profile, Security, System) are stacks of
    sheets separated by a rule. Where there is room (a 53rem stack) a sheet is two columns: its name and a
    line about it on the left, its fields on the right, no wider than 34rem. On a tablet it is one column.
    The save bar is a row on the page's lower edge with its state in words beside the button. Settings and
    Maintenance share a General / Maintenance tab row.
  • Figures on Stats. Views, reads and the read ratio are one ruled row of large figures; a change is
    green, a note is not. The chart, the share lists and the table of posts and pages lose their boxes;
    a share is a thin green line under its label.
  • Three empty states. A screen with nothing made yet shows a short block with the one action that
    starts it. A tab or filter that finds nothing says so in a line, with a link back to all. An empty part
    of a form is a muted line.
  • Smaller things. Card and row titles use the display face; status is a dot and a word, with no
    pill; numbers are set in columns; a list states its timezone once, under its title, instead of on
    every card; spacing follows an 8px rhythm, with 32px added to the admin's steps.

Removed

  • The admin's post search. The field in the top bar searched post titles only but sat on every
    screen, so it read as a search over everything. It is gone, with the title filter on Pages. A q in
    an address the list already had still filters it.
  • "Apply filters". Posts and Pages keep their status tabs and a language filter at the end of the tab
    row. Chosen with the mouse, a language applies at once. With the keyboard, arrowing through the
    languages does not leave the page; Tab to "Show this language" to apply the choice. Without JavaScript
    that button is the way for everyone.

The public themes

On Paper, the search box moves from above the category pills to the right end of their row: a quiet
field 15rem wide that grows to 20rem while it has focus, with the magnifier inside it as the submit
button. On a phone the field takes the full width above the pills. Plain does the same on its own row
and keeps its text button. The results line, the empty state, noindex on a results page and the
paging are unchanged.

For theme authors

Nothing in the theme contract changed. The bundled themes' markup did: Paper's .post-search__field
wrapper is gone and the form now sits inside .post-controls with nav.post-filter; Plain's form sits
inside .plain-controls. A theme that copied either bundled theme's home page keeps working as it is.

Testing

  • tests/unit/admin-surface-tokens.test.ts pins the new shapes, and tests/unit/theme-contrast.test.ts
    the colour pairs they lean on, in both themes.
  • tests/e2e/admin-shots.spec.ts shoots every admin screen at 1440, 768 and 375 pixels in both themes and
    measures the boxes the design makes claims about. It runs only when asked:
    ADMIN_SHOTS=<label> npm run test:e2e -- tests/e2e/admin-shots.spec.ts --project=desktop writes to
    .superpowers/admin-shots/<label>/.
  • The language filter and the "Show all" link from an empty tab are covered in
    tests/e2e/select-in-dialog.spec.ts.

TomeCMS 1.3.3

Choose a tag to compare

@github-actions github-actions released this 29 Sep 16:31
Immutable release. Only release title and notes can be modified.

TomeCMS 1.3.3

Date: 2026-09-29
Status: Stable release; tagged and published as a GitHub release

TomeCMS 1.3.3 makes Paper's reading progress bar and fading hero work on a real site, and cuts the
database queries of every public page by more than half. /api/v1 is unchanged.

Upgrading

  • A managed install updates from "System" in the admin, as
    Updating describes. There is no
    migration, no new setting and no new runtime dependency.
  • A build from source upgrades in place by running the deploy helper again.

deploy/cloud-init.yaml now installs 1.3.3.

The reading progress bar and the fading hero

Paper's "Reading progress bar" and the "Fading" choice under "Slides change by" draw from the scroll
position with a scroll-driven animation, and neither ever moved on a production site. The owner of the
test site ticked the bar in Chrome and saw nothing. On that page the bar was rendered, and its
animation-name was none.

The build minifies CSS, and the minifier folds animation and animation-timeline into one
animation shorthand with the timeline inside it. Chrome does not accept a timeline in that shorthand,
so it drops the whole declaration. The browser tests passed because they run against the development
server, which does not minify.

Both animations are written as longhands now (animation-name, animation-timing-function,
animation-fill-mode, animation-timeline), which the minifier keeps apart. A unit test minifies every
theme stylesheet with the same minifier and fails on a timeline inside an animation shorthand. On a
local production build the bar now fills in proportion to the scroll.

Fewer queries per page

1.3.2 made the home page measure reading time once per post, which was 2.7 times faster locally with
Thai posts but did not move the test server (17.5 requests a second before, 18.2 after). Measuring
there instead showed the CPU split between the application and PostgreSQL, and a home page sending 16
queries: the site's settings six times, and each plugin's settings in its own query.

getSiteSettings and the enabled plugins' settings are now read once per request, through a scope the
middleware opens for GET and HEAD requests only; the plugins are read in one query, and a theme's
settings come from the same row. Nothing is remembered past the request, so an edit is seen by the next
one and there is nothing to invalidate. The home page now sends 6 queries, a post's page 5 (from 14), and
the content API 4 (from 5).

Locally, where PostgreSQL runs on other cores than the application, a post's page went from about 660
to 840 requests a second and the home page from 246 to 271. On a one-core server, where both share the
CPU, the saving should count for more; it has not been measured there yet.

Validation boundary

  • npm run check passes, and the documentation site's check and build pass.
  • npm run test:unit: 636 tests, all passing, among them the per-request memo (one read per request,
    none kept across requests or outside one, a failed read not kept) and the minified-CSS check.
  • Integration tests on a real database for plugin settings, the popup, published queries, search and
    maintenance pass, and so do the browser tests of the public plugins, theme settings, the brand, the
    theme toggle and maintenance.
  • On a local production build the reading progress bar was seen to fill as the page scrolled.
  • Not yet measured on the managed server.

TomeCMS 1.3.2

Choose a tag to compare

@github-actions github-actions released this 29 Sep 15:23
Immutable release. Only release title and notes can be modified.

TomeCMS 1.3.2

Date: 2026-09-29
Status: Stable release; tagged and published as a GitHub release

TomeCMS 1.3.2 makes the Paper home page about two and a half times as fast for a site written in
Thai. Nothing else changes, and /api/v1 is unchanged.

Upgrading

  • A managed install updates from "System" in the admin, as
    Updating describes. There is no
    migration, no new setting and no new runtime dependency. From 1.3.0 with an upgraded updater, the
    backup is the database alone.
  • A build from source upgrades in place by running the deploy helper again.

deploy/cloud-init.yaml now installs 1.3.2.

What was slow

Measured on the 1 GB test server, the home page answered 17.5 requests a second, four at a time,
about a quarter of what the same page did with English test posts. Every card shows "N min read",
and the page worked that out on every request by splitting the whole text of each post into words
with Intl.Segmenter. Thai is written without spaces, so that runs a dictionary word breaker over the
entire post: about 4.4 ms for a long Thai post on a fast machine, against 2.2 ms for English, for
every card, for every reader.

The fix

The reading time of a post changes only when the post does. It is now worked out once per saved
version (the post's id and last update) and remembered in memory for the newest 1,000 versions. An
edit is measured afresh; nothing else is.

Measured locally, the production build with 60 long Thai posts: the home page went from 92 to 246
requests a second, and the Node CPU it took from 10.9 ms to 3.4 ms a request. A post's own page, which
never measured reading time, is unchanged at about 660.

What remains of the home page's cost is mostly reading full post bodies from the database for cards
that show only a title and an excerpt. That is a larger change, left until it is measured to matter.

Validation boundary

  • npm run check passes, and the documentation site's check and build pass.
  • npm run test:unit: 633 tests, all passing, among them that a post's reading time is measured once
    per version and again after an edit.
  • The browser tests of the Paper home page and of search pass.
  • Not yet measured on the managed server: the home page on the test server after this update.

TomeCMS 1.3.1

Choose a tag to compare

@github-actions github-actions released this 29 Sep 14:15
Immutable release. Only release title and notes can be modified.

TomeCMS 1.3.1

Date: 2026-09-29
Status: Stable release; tagged and published as a GitHub release

TomeCMS 1.3.1 changes documentation only. The application is the same as 1.3.0, and /api/v1 is
unchanged. It is also the first release a 1.3.0 server installs with the lighter backup.

Upgrading

  • A managed install updates from "System" in the admin, as
    Updating describes. There is no
    migration, no new setting and no new runtime dependency. On a server that runs 1.3.0 and whose
    updater was upgraded to 1.3.0, this update backs up the database alone, since no migration is due,
    and afterwards the "Last update" card says how long the site was offline.
  • A build from source upgrades in place by running the deploy helper again.

deploy/cloud-init.yaml now installs 1.3.1.

Documentation

  • Upgrading the updater on a server installed before 1.0.2: such a server has no
    /opt/tome-cms-src, so the release is cloned there first. The first real run of
    npm run updater:upgrade, on the test server, found that gap, and the clone worked.
  • What the server needs now says that a managed install runs a personal site on 1 GB of memory
    with 2 GB of swap. The test server (1 GB, one shared vCPU) has run a managed install with real
    content since 1.0.1. With the site in use it had about 400 MB available and swap in use; its home
    page answered about 17 requests a second, four at a time, with no failures. The minimum stays 2 GB
    for a busier site and for a build from source.
  • The 1.0.0 acceptance record has its first real-server row: the managed install on the test
    server, its updates from the admin up to 1.3.0, the updater upgrade from 1.0.0 to 1.3.0, and
    npm run restore:check verifying both the newest and the oldest updater backup in disposable
    projects.

Validation boundary

  • npm run check passes, and the documentation site's check and build pass.
  • npm run test:unit: 631 tests, all passing.
  • Not yet run on the managed server: updating it from 1.3.0 to 1.3.1 is the first database-only
    backup on real hardware.

TomeCMS 1.3.0

Choose a tag to compare

@github-actions github-actions released this 29 Sep 12:09
Immutable release. Only release title and notes can be modified.

TomeCMS 1.3.0

Date: 2026-09-29
Status: Stable release; tagged and published as a GitHub release

TomeCMS 1.3.0 makes updates without a migration keep the site offline for less time, shows how long
an update kept it offline, and lets the managed updater itself be upgraded. /api/v1 is unchanged.

Upgrading

  • A managed install updates from "System" in the admin, as
    Updating describes. There is no
    migration, no new setting and no new runtime dependency. An updater of any 1.x version installs it,
    with a full backup as before.

  • Then upgrade the updater, once, from a checkout of v1.3.0, as
    Upgrading the updater
    shows. The site stays up while it runs:

    cd /opt/tome-cms-src
    git fetch --depth 1 origin tag v1.3.0
    git checkout --detach v1.3.0
    npm ci
    sudo npm run updater:upgrade -- --dry-run
    sudo npm run updater:upgrade

    A server installed with 1.0.1 or 1.0.2 also gets the updater fixes of 1.0.3 this way (the stop
    timeout and the rollback that waits for it), which it never had. Nothing requires it: without it the
    next updates work as before, with a full backup.

  • A compose file edited by hand is replaced by the release's own. The command says so when it
    was different and keeps your copy beside it. The only change since 1.0.0 is init: true under
    app:, which a server that followed the 1.0.2 troubleshooting entry already has.

  • A build from source upgrades in place by running the deploy helper again.

deploy/cloud-init.yaml now installs 1.3.0.

A lighter backup when no migration is due

The updater stops the application, backs up, migrates and starts it again, and the site is offline
from the stop to the end. The backup copied the database and every file in the media library, so a
site with a large library was offline for as long as that copy took, even for an update that changed
nothing in the database.

From 1.3.0 the updater first lists the migrations the installed application ships and compares them
with the new release's, before the site goes offline. When they are the same, no migration will run
and no table changes, so it backs up the database alone. When a migration is due, or the list cannot
be read, the backup is full as before. The backup runs inside the installed application, whose own
backup command has to know --database-only, so it takes effect from an update that starts on 1.3.0
or later: 1.3.0 to 1.3.1, not 1.2.1 to 1.3.0.

A database-only backup has no objects/ folder, and its manifest.json says "scope": "database".
npm run restore:check checks it and says so. To restore one, restore the dump and leave the bucket as
it is, as Backups and restore now says. A full
backup is written exactly as before, so an older restore check still reads it.

How long the site was offline

With an updater of 1.3.0 or later, each update records when every phase began. The "Last update"
card on "System" then says how long the site was offline, from maintenance to the end, and what the
backup held and how long it took, such as "Database only, no migration was due (3 s)". The record is
private to the server and read through a new updater route, /v1/timeline; the updater's status keeps
its old shape, so an application from before 1.3.0 reads it as before.

Before this release the only measurement came from daedalus, the test server: its 1.2.0 update took
80 seconds from start to end, download included, with backups of about 9 MB each.

Upgrading the updater

The updater runs on the host and was built once, when the server was installed; updating from
"System" replaces the application image only. sudo npm run updater:upgrade:

  • runs only as root, from a clean checkout of the exact release tag, and refuses to go back to an
    older updater;
  • builds the updater, stops the service, checks again that no update started meanwhile (and starts
    the service and stops if one did), then replaces /opt/tome-cms/updater, the systemd unit and
    compose.managed.yaml with the release's own, keeping the old ones with .previous- and the time;
  • starts the service and waits up to 30 seconds for it to answer with its new version; if it does
    not, it puts every old file back and starts the old updater, and a restore step that fails does not
    stop the ones after it.

--dry-run checks and says what it would replace, and changes nothing.

Validation boundary

  • npm run check passes, and the documentation site's check and build pass.
  • npm run test:unit: 631 tests, all passing, among them: the job timeline and backup kind kept apart
    from the public status, a job file written by an earlier updater still read; the backup kind chosen
    before the site goes offline, full for an installed application before 1.3.0, for a migration, or
    when the installed image cannot say its migrations; a backup of the wrong kind refused before the
    image changes, both ways; the database-only manifest; and the upgrade's refusals and file swap,
    including an update that starts while the new updater builds and a restore step that fails.
  • npm run test:operations:update (real containers, a full update and a rollback): 9 tests pass.
  • The upgrade was rehearsed in a Linux container with a stand-in for systemctl: an updater built from
    v1.2.1 answered 1.0.0, the upgrade refused while a job was in progress, the dry run changed nothing,
    the upgrade replaced it with 1.3.0 and kept the old files, and a new updater that crashed at start
    was replaced by the old one, which answered again.
  • A browser test shows the offline time and the backup kind on "System".
  • Not yet run on the managed server: installing 1.3.0, upgrading the updater there, and the first
    database-only backup (1.3.0 to the next release) are their first runs on real hardware.

TomeCMS 1.2.1

Choose a tag to compare

@github-actions github-actions released this 29 Sep 10:52
Immutable release. Only release title and notes can be modified.

TomeCMS 1.2.1

Date: 2026-09-29
Status: Stable release; tagged and published as a GitHub release

TomeCMS 1.2.1 fixes three small things found while reviewing 1.1.1 and 1.2.0. There is no new
feature, and /api/v1 is unchanged.

Upgrading

  • A managed install updates from "System" in the admin, as
    Updating describes. There is no
    migration, no new setting and no new runtime dependency.
  • A build from source upgrades in place by running the deploy helper again.

deploy/cloud-init.yaml now installs 1.2.1.

Fixed

  • A re-check after the session ended. With the Cloudflare Turnstile plugin on, a passkey
    check on "System", on "Security" or for new recovery codes, made after the session had ended,
    said "The sign-in check was not passed" and suggested switching the plugin off on the server.
    Only the sign-in page can pass that check, so the advice could never help and pointed at
    disabling a security control. It now says "The sign-in session expired. Reload the page and try
    again."
  • Who skips the sign-in challenge. Since 1.1.1 an owner who is signed in is not asked for the
    challenge again. The server now skips it only for a session that belongs to the installed owner,
    rather than for any session that exists. No session other than the owner's reaches that point
    today; this keeps it so if one ever does.
  • Plain's secondary text. The Plain theme read --color-ink-muted, which no stylesheet
    declares, so its tagline, dates, excerpts, category filter, footer and search status were drawn
    in full ink. They use --color-muted again, whose contrast on every page surface is already
    pinned at 4.5:1 or more in both colour schemes.

Validation boundary

  • npm run check passes, and the documentation site's check and build pass.
  • npm run test:unit: 612 tests, all passing, among them the wording of a refused re-check in
    both languages, that the challenge skip is keyed on the owner's identity, and a new test that
    fails on any theme stylesheet reading a custom property nothing declares.
  • The browser tests for re-authentication with Turnstile on and for search in both themes pass;
    the Plain homepage was looked at with its muted text restored.

TomeCMS 1.2.0

Choose a tag to compare

@github-actions github-actions released this 29 Sep 07:38
Immutable release. Only release title and notes can be modified.

TomeCMS 1.2.0

Date: 2026-09-29
Status: Stable release; tagged and published as a GitHub release

TomeCMS 1.2.0 lets readers search the posts, from the homepage of both bundled themes and from the
public API. Every 1.1.x feature keeps working. The one change to /api/v1 is an optional
parameter, q.

Upgrading

  • A managed install updates from "System" in the admin, as
    Updating describes. There is no
    migration, no new setting and no new runtime dependency. An update started from 1.1.1 or later
    works with the Cloudflare Turnstile plugin on.
  • A headless site does not have to change. q is optional, a list without it answers as it
    did, and a cursor issued before this release still opens.
  • A theme of your own, in a fork, must accept the new query prop of Home, which the route
    always passes. Themes says what a
    Home template does with it. Paper and Plain already do.
  • Behind a proxy, the limit on searching counts each sender by the address the proxy writes in
    X-Forwarded-For, as the limits on signing in have since 1.1.2.

deploy/cloud-init.yaml now installs 1.2.0.

Search

  • The box. Paper and Plain show a search box above the posts on the homepage. It is a plain
    form, so it works without JavaScript, and the words stay in the box. Above the results a line
    says Results for “…” with a link, "Clear search", back to the whole list. Paper hides its hero
    while results are shown, so they come first. When nothing matches, the page says so, and the
    reader's words are always printed as text.
  • What it finds. The published posts of the page's language that contain every word typed,
    wherever the word is: the title, the excerpt or the text. Case does not matter, and a word
    matches inside longer words, which is what makes it work in Thai, where words are not separated
    by spaces. A search takes at most five words and 100 characters. %, _ and \ are only
    themselves.
  • What counts as text. Bold, italic, a link or a colour inside a word does not cut it, so a
    Thai phrase is found across a bold word in it. A paragraph, a list item, a table cell or a line
    break does end a word. Class names, addresses and tag names are not text, and an entity such as
    &amp; is the character it shows, so a search for Q&A finds it.
  • The results page. It is kept out of search engines (noindex, follow) and its title says
    what was searched for. "Older posts" and the endless scroll keep the search, and choosing a
    category leaves it, in the list and in the box.
  • The API. GET /api/v1/content/posts?q= takes the same words. links.next keeps q, the
    signed cursor is tied to it, and an empty q or one with no word in it is a 400. The OpenAPI
    document and the API overview
    describe it.
  • Limits. A search reads every published post of the language, so a sender may make 60 a
    minute, and no more than two searches run at once. Past either, the page answers a plain
    429, and the API a problem response with a Retry-After of 60 seconds. A list with no search
    is never held back. A headless site that fetches from its own server is one sender for all its
    visitors, so it should let the visitor's browser call the API or keep what it fetched for a
    minute.

For theme authors and headless sites

  • ThemeHomeProps gains query: string | undefined, the words that were searched for. A Home
    template draws a role="search" form whose field is named q, says what was searched for and
    how to leave the search, carries q on its link to older posts, and draws the results before
    any hero. The route narrows posts and sends the noindex; the template needs nothing for those.
  • Nothing was removed or renamed.

Validation boundary

  • npm run check passes, and the documentation site's check and build pass, links included.
  • npm run test:unit: 609 tests, all passing, among them the splitting and escaping of a search,
    the query the API accepts and refuses, that a list with no search hashes its cursor as before,
    and that only two searches run at once.
  • A new integration test on a real database covers a search over the title, the excerpt and the
    body; Thai and English apart; drafts and scheduled posts left out; tags, class names and
    addresses not searched; entities and the %, _, ! and \ characters; a Thai phrase across a
    bold word, and paragraphs, list items and cells kept apart; several words, categories and
    cursors; and the API over HTTP, including that a refused query costs the sender nothing.
  • A new browser test drives both themes at 375 and 1440 px: the box, the results page, nothing
    found, a hostile string with quotes and $&, no JavaScript, the older posts keeping the search,
    a category leaving it, Thai, no sideways scroll, and the limit. The existing browser tests of the
    homepage, the theme settings, the public plugins and Stats still pass.
  • The screens were looked at in a browser, in both themes, at both widths.
  • Not yet run on the managed server: updating it to 1.2.0 is its first run on real hardware.

TomeCMS 1.1.2

Choose a tag to compare

@github-actions github-actions released this 29 Sep 06:37
Immutable release. Only release title and notes can be modified.

TomeCMS 1.1.2

Date: 2026-09-29
Status: Stable release; tagged and published as a GitHub release

TomeCMS 1.1.2 makes the limits on signing in, recovery, installing and updating count each
visitor, not the reverse proxy in front of the application. Nothing else changes, and /api/v1
is unchanged.

Upgrading

  • A managed install updates from "System" in the admin, as
    Updating describes. There is no
    migration, no new setting and no new runtime dependency. Caddy already sends what the change
    reads. The Cloudflare Turnstile plugin no longer needs to be switched off for this update if
    the site is on 1.1.1; it does if the site is on 1.1.0 or earlier.
  • Behind another proxy, make it set or append X-Forwarded-For, as
    the reverse proxy
    says. A proxy that leaves it out keeps the old behaviour, one count for every visitor. A proxy
    that passes a visitor's own header through untouched, which nginx does without a line for it,
    lets that visitor choose the address the limits count, so set it before updating.
  • A build from source upgrades in place by running the deploy helper again.

deploy/cloud-init.yaml now installs 1.1.2.

What was wrong

Astro's node adapter hands the application the address of the socket, and behind the proxy that
is the proxy, Docker's bridge on a managed install, for every visitor. It believes
X-Forwarded-For only when the site's domains are listed when the image is built, which one
official image serving every domain cannot do. So each limit was a single count for the whole
internet: 10 sign-in attempts in 15 minutes, 5 recovery attempts in 30, 8 for installing, 6 update
checks in 10 minutes and 3 installs in 30. Ten failing sign-in requests from anyone kept the owner
out for up to fifteen minutes (a passkey cannot be guessed that way, but the owner could not sign
in), and the Cloudflare Turnstile plugin was told the proxy's address as the visitor's.

The fix

  • senderAddress, which Stats has used since 0.11 to count readers, now serves every limit and
    the sign-in challenge. It takes the last X-Forwarded-For entry, the one the proxy wrote, and
    only when the connection comes from the proxy's side: loopback, 10.0.0.0/8, 172.16.0.0/12,
    192.168.0.0/16 or fc00::/7. From anywhere else the header is the sender's own words and is
    ignored. The limit takes only an address senderAddress returned, which is a type, so a route
    that hands it the peer's address does not compile.
  • A visitor on IPv6 counts by their /64, as Stats already did, so cycling through the addresses
    of one block buys no fresh count.
  • Rows of the limit table older than an hour are swept, once a minute at most. With a count per
    visitor the table would otherwise grow with every address that ever asked.
  • security.allowedDomains stays unset. The helper depends on that, a test now says so, and the
    comment on it says why setting it could not be the fix.

Validation boundary

  • npm run check passes, and the documentation site's check and build pass, links included.
  • npm run test:unit: 603 tests, all passing, among them the proxy-side rules, the /64 key, and
    that the app never lists its domains.
  • A new integration test on a real database drives the sign-in route from Docker's bridge: ten
    attempts from one visitor pass and the eleventh is refused, another visitor behind the same
    proxy is not limited, a made-up header from a public address buys no fresh count, one /64 counts
    as one visitor however its addresses are spelled, and a row from two hours ago is swept. Before
    the change the second visitor was refused with the first.
  • The production build, node dist/server/entry.mjs, behind a real TCP proxy that overwrites
    X-Forwarded-For as Caddy does: the same result over HTTP, and a header the client wrote itself
    was ignored.
  • Not yet run on the managed server: updating it to 1.1.2 is its first run on real hardware.

TomeCMS 1.1.1

Choose a tag to compare

@github-actions github-actions released this 29 Sep 05:10
Immutable release. Only release title and notes can be modified.

TomeCMS 1.1.1

Date: 2026-09-29
Status: Stable release; tagged and published as a GitHub release

TomeCMS 1.1.1 fixes installing an update from the admin on a site that has the Cloudflare
Turnstile plugin switched on. Nothing else changes, and /api/v1 is unchanged.

Upgrading

  • A managed install updates from "System" in the admin, as
    Updating describes. There is no
    migration, no new setting and no new runtime dependency.
  • On 1.1.0 or earlier with Turnstile on, switch "Cloudflare Turnstile" off under "Plugins"
    before you press install, and on again afterwards. The running version is the one that asks for
    the passkey, so this one update still needs it.
    Troubleshooting
    has the steps. An update started from 1.1.1 does not.
  • A build from source upgrades in place by running the deploy helper again.

deploy/cloud-init.yaml now installs 1.1.1.

What was wrong

On 1.1.0 and earlier, with the Turnstile plugin on, confirming an update with your passkey on
"System" failed with "The passkey check did not finish. It may have been cancelled. Try again.",
although the passkey was fine. "Verify and create new codes", under "Recovery codes" on "Security", failed the same way. Asking for a passkey once
more goes through the same request as signing in, the server put the sign-in challenge in front of
it, and only the sign-in page can pass the challenge, so the request came back 403 with
challenge_refused. On "System" the failure was also written to the line that reports the release
check, so "Release availability" turned red and said "Check unavailable" although the check had
worked.

The fix

  • The sign-in challenge is asked for when there is no session. An owner who is already signed in
    is confirming a passkey again, not signing in, and is not who the challenge keeps out. The rate
    limit on sign-in attempts still applies to every such request, and a caller with no session meets
    the challenge as before.
  • "System" reports a failed passkey check as the other passkey screens do: a refused challenge, a
    rejected origin, a rate limit, a passkey the site does not know, a server fault and a lost
    connection each get their own sentence, in red beside the buttons. "Release availability" keeps
    saying "Update available", because the release check did work.

Validation boundary

  • npm run check passes, and the documentation site's check and build pass, links included.
  • npm run test:unit: 602 tests, all passing.
  • A new browser test, tests/e2e/reauth-with-challenge.spec.ts, runs on a disposable stack with a
    passkey registered and Turnstile switched on. It confirms an update on "System" and makes new
    recovery codes, and it checks that a caller with no session is still refused, whether the caller
    sends no cookie, a made-up one, or the cookie of a session that has just ended. It also checks that
    a refused passkey is named on "System" while "Release availability" stays true. Before the fix,
    confirming an update and making new recovery codes failed with 403 challenge_refused, and a
    refused passkey had no message of its own; after it, all four tests pass.
  • Not yet run on the managed server: installing this update there is its first run on real
    hardware.