Skip to content

Reading lists / bookmarks for readers - #249

Merged
nwwatson merged 1 commit into
masterfrom
automated/issue-41-253852
Sep 18, 2026
Merged

nwwatson merged 1 commit into
masterfrom
automated/issue-41-253852

Conversation

@nwwatson

Copy link
Copy Markdown
Owner

Summary

Readers can bookmark posts (icon on every post card and on the post page) and see them at /reading-list, linked from a new bookmark icon in the masthead. Readers without an account keep their list in localStorage. Signed-in readers' lists are stored in the database. The first time someone loads a page signed in, any posts saved on that device are merged into their account, so the list follows them across devices.

Changes

  • Model: ReadingListItem (identity ↔ post, unique per pair, capped at 500 per identity) + Identity::ReadingListable (bookmark!, unbookmark!, reading_list_posts, reading_list_post_ids, import_reading_list!). Only live posts are ever listed.
  • Endpoints: GET /reading-list (page), GET /reading-list/posts?ids= (cards for a localStorage list, rendered into a turbo frame), POST/DELETE /reading-list/items and POST /reading-list/import (JSON, for signed-in readers; they return 401 JSON when signed out).
  • JS: lib/reading_list.js holds the list state for the whole page. bookmark_controller handles one button. reading_list_controller loads a signed-out reader's saved posts onto the page.
  • Views: bookmark button in posts/_post and posts/show; masthead icon group (.masthead__actions); new BEM components _bookmark-btn.css and _reading-list.css; en + es strings; reading-list added to reserved page slugs.

Design notes / assumptions

  • Keyed on identity_id, not subscriber_id as the issue suggested. This matches Love and Comment, and it means staff users signed into the public site get a reading list too.
  • Button state is set in the browser, not by the server. Post cards are fragment-cached and shared by every reader, so the button is rendered hidden and identical for everyone, and JS fills in whether it's saved. A test checks that a cached card never carries one reader's saved state to another.
  • Signed-in state is kept in JS memory, not re-read from the <meta> tag on every page. When a reader hits Back, Turbo restores an old copy of the page head, which can be from before their last save. The meta is only read again when its data-key changes, i.e. on sign-in or sign-out.
  • Removing a post on the reading-list page leaves its card in place (icon unfilled) until the next visit, so an accidental click can be undone.

Out of scope

  • Reading lists are not included in the JSON export or the admin UI.
  • The button needs JavaScript; without it the button stays hidden.

Documentation

  • README.md: added a Reading List entry under Features.
  • CLAUDE.md: new "Reading List (Bookmarks)" architecture section, model list entries, Stimulus controller list.

Testing

  • Unit/controller tests for the model, concern, all four controllers, the button markup, and the fragment-cache safety check; system tests for saving without an account, un-saving, and the merge on sign-in followed by saves stored on the server.
  • Manual: visit / signed out, click a bookmark icon, open the masthead bookmark → the post is listed. Sign in as a subscriber via magic link → the post is still saved and is now in reading_list_items.
  • bin/rails test (1456 runs) and bin/rails test:system (31 runs) pass
  • Rubocop: no offenses; Brakeman, bundler-audit, importmap audit clean

Closes #41

🤖 Generated with Claude Code

Readers bookmark posts from post cards and post pages and view them at
/reading-list. Anonymous readers' lists live in localStorage; signed-in
readers (subscribers and staff, keyed on Identity) are stored in the new
reading_list_items table, and a device's list is merged into the account
the first time the reader loads a page signed in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@nwwatson nwwatson self-assigned this Sep 18, 2026
@nwwatson
nwwatson merged commit d4fb73e into master Sep 18, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Reading lists / bookmarks for readers

1 participant