diff --git a/CHANGELOG.md b/CHANGELOG.md
index 813324e..d664e16 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,15 @@ All notable changes to Tome are documented here. Format loosely follows
## [Unreleased]
+### Added
+- **Preview a book without touching your progress.** The Read button on the
+ book page now has a small menu with "Preview": the book opens in the web
+ reader as usual, but nothing is written back. Reading status, position, the
+ position synced to your devices, reading time and stats all stay as they
+ were, so you can check a file's formatting or flip through a book to decide
+ on it without it turning up as started. The reader shows a "Preview" tag
+ while this is on. Highlights and notes still work. (#289)
+
## [2.8.0] - 2026-10-24 - "Folio"
### Added
diff --git a/frontend/e2e/preview-reading.spec.ts b/frontend/e2e/preview-reading.spec.ts
new file mode 100644
index 0000000..1326faf
--- /dev/null
+++ b/frontend/e2e/preview-reading.spec.ts
@@ -0,0 +1,50 @@
+import { test, expect } from '@playwright/test'
+import { seed, login, apiToken, apiGet } from './helpers'
+
+// Preview (#289): the reader opened with ?preview=1 must not write anything
+// back. Every write the web reader makes is PUT /books/{id}/status, so the
+// test counts those and checks the stored status afterwards.
+test.describe('preview reading', () => {
+ test('a normal open marks the book reading, a preview leaves it untouched', async ({ page, request }) => {
+ seed('many', 2)
+ await login(page)
+ const token = await apiToken(request)
+ const { body: books } = await apiGet<{ id: number; title: string }[]>(request, token, '/api/books?limit=10')
+ const [normal, preview] = books
+
+ const puts: string[] = []
+ await page.route(/\/api\/books\/\d+\/status$/, route => {
+ if (route.request().method() === 'PUT') puts.push(route.request().url())
+ return route.continue()
+ })
+
+ // Control: the normal reader writes "reading" as soon as it opens.
+ await page.goto(`/reader/${normal.id}`)
+ await expect.poll(() => puts.length, { timeout: 10_000 }).toBeGreaterThan(0)
+ const after = await apiGet<{ status: string }>(request, token, `/api/books/${normal.id}/status`)
+ expect(after.body.status).toBe('reading')
+
+ // Preview: same reader, nothing written.
+ puts.length = 0
+ await page.goto(`/reader/${preview.id}?preview=1`)
+ await page.waitForTimeout(4000)
+ expect(puts).toHaveLength(0)
+ const kept = await apiGet<{ status: string; progress_pct: number | null }>(request, token, `/api/books/${preview.id}/status`)
+ expect(kept.body.status).toBe('unread')
+ expect(kept.body.progress_pct).toBeNull()
+ })
+
+ test('the Read button menu opens the preview', async ({ page, request }) => {
+ seed('many', 1)
+ await login(page)
+ const token = await apiToken(request)
+ const { body: books } = await apiGet<{ id: number }[]>(request, token, '/api/books?limit=1')
+
+ await page.goto(`/books/${books[0].id}`)
+ await expect(page.getByRole('button', { name: 'Read', exact: true })).toBeVisible()
+ await page.getByRole('button', { name: 'More ways to read' }).click()
+ await expect(page.getByText('Does not update progress or stats')).toBeVisible()
+ await page.getByText('Preview', { exact: true }).click()
+ await expect(page).toHaveURL(new RegExp(`/reader/${books[0].id}\\?preview=1$`))
+ })
+})
diff --git a/frontend/src/locales/en/messages.po b/frontend/src/locales/en/messages.po
index fdd3ff2..d4c06df 100644
--- a/frontend/src/locales/en/messages.po
+++ b/frontend/src/locales/en/messages.po
@@ -2891,6 +2891,10 @@ msgstr "Do you rate books you spend longer on higher?"
msgid "Docs"
msgstr "Docs"
+#: src/pages/BookDetailPage.tsx
+msgid "Does not update progress or stats"
+msgstr "Does not update progress or stats"
+
#: src/components/ConnectPhoneModal.tsx
#~ msgid "Don't have it yet? Tome Reader is in open beta for iPhone."
#~ msgstr "Don't have it yet? Tome Reader is in open beta for iPhone."
@@ -4579,6 +4583,10 @@ msgstr "More"
msgid "More send options"
msgstr "More send options"
+#: src/pages/BookDetailPage.tsx
+msgid "More ways to read"
+msgstr "More ways to read"
+
#: src/pages/StatsPage.tsx
msgid "Morning vs evening reader?"
msgstr "Morning vs evening reader?"
@@ -5737,6 +5745,8 @@ msgid "Preparing…"
msgstr "Preparing…"
#: src/components/MetadataFetchModal.tsx
+#: src/pages/BookDetailPage.tsx
+#: src/pages/ReaderPage.tsx
#: src/pages/SettingsPage.tsx
msgid "Preview"
msgstr "Preview"
@@ -5745,6 +5755,10 @@ msgstr "Preview"
msgid "Preview:"
msgstr "Preview:"
+#: src/pages/ReaderPage.tsx
+msgid "Preview: progress and stats are not updated"
+msgstr "Preview: progress and stats are not updated"
+
#: src/components/MetadataManager.tsx
#: src/pages/AdminPage.tsx
msgid "Previous"
diff --git a/frontend/src/locales/fr/messages.po b/frontend/src/locales/fr/messages.po
index 3d7d633..63939d8 100644
--- a/frontend/src/locales/fr/messages.po
+++ b/frontend/src/locales/fr/messages.po
@@ -2882,6 +2882,10 @@ msgstr "Notez-vous mieux les livres auxquels vous consacrez plus de temps ?"
msgid "Docs"
msgstr "Documentation"
+#: src/pages/BookDetailPage.tsx
+msgid "Does not update progress or stats"
+msgstr ""
+
#~ msgid "Don't have it yet? Tome Reader is in open beta for iPhone."
#~ msgstr "Don't have it yet? Tome Reader is in open beta for iPhone."
@@ -4560,6 +4564,10 @@ msgstr "Plus"
msgid "More send options"
msgstr "Autres options d’envoi"
+#: src/pages/BookDetailPage.tsx
+msgid "More ways to read"
+msgstr ""
+
#: src/pages/StatsPage.tsx
msgid "Morning vs evening reader?"
msgstr "Plutôt lecture du matin ou du soir ?"
@@ -5713,6 +5721,8 @@ msgid "Preparing…"
msgstr "Préparation…"
#: src/components/MetadataFetchModal.tsx
+#: src/pages/BookDetailPage.tsx
+#: src/pages/ReaderPage.tsx
#: src/pages/SettingsPage.tsx
msgid "Preview"
msgstr "Aperçu"
@@ -5721,6 +5731,10 @@ msgstr "Aperçu"
msgid "Preview:"
msgstr "Aperçu :"
+#: src/pages/ReaderPage.tsx
+msgid "Preview: progress and stats are not updated"
+msgstr ""
+
#: src/components/MetadataManager.tsx
#: src/pages/AdminPage.tsx
msgid "Previous"
diff --git a/frontend/src/locales/zh-CN/messages.po b/frontend/src/locales/zh-CN/messages.po
index 7faf800..8d01d5c 100644
--- a/frontend/src/locales/zh-CN/messages.po
+++ b/frontend/src/locales/zh-CN/messages.po
@@ -2891,6 +2891,10 @@ msgstr ""
msgid "Docs"
msgstr ""
+#: src/pages/BookDetailPage.tsx
+msgid "Does not update progress or stats"
+msgstr ""
+
#: src/components/ConnectPhoneModal.tsx
#~ msgid "Don't have it yet? Tome Reader is in open beta for iPhone."
#~ msgstr ""
@@ -4579,6 +4583,10 @@ msgstr ""
msgid "More send options"
msgstr ""
+#: src/pages/BookDetailPage.tsx
+msgid "More ways to read"
+msgstr ""
+
#: src/pages/StatsPage.tsx
msgid "Morning vs evening reader?"
msgstr ""
@@ -5737,6 +5745,8 @@ msgid "Preparing…"
msgstr ""
#: src/components/MetadataFetchModal.tsx
+#: src/pages/BookDetailPage.tsx
+#: src/pages/ReaderPage.tsx
#: src/pages/SettingsPage.tsx
msgid "Preview"
msgstr ""
@@ -5745,6 +5755,10 @@ msgstr ""
msgid "Preview:"
msgstr ""
+#: src/pages/ReaderPage.tsx
+msgid "Preview: progress and stats are not updated"
+msgstr ""
+
#: src/components/MetadataManager.tsx
#: src/pages/AdminPage.tsx
msgid "Previous"
diff --git a/frontend/src/pages/BookDetailPage.tsx b/frontend/src/pages/BookDetailPage.tsx
index 959dcff..85910cb 100644
--- a/frontend/src/pages/BookDetailPage.tsx
+++ b/frontend/src/pages/BookDetailPage.tsx
@@ -6,7 +6,7 @@ import {
Calendar, Globe, Hash, Building2, FileText, Trash2, Loader2,
Sparkles, Library, Check, BookMarked, ChevronLeft, ChevronRight, Home,
Tag as TagIcon, StickyNote, ChevronDown, Archive, AlignLeft,
- Plus, TrendingUp, TrendingDown, Minus, History as HistoryIcon, Share2, Timer
+ Plus, TrendingUp, TrendingDown, Minus, History as HistoryIcon, Share2, Timer, Eye
} from 'lucide-react'
import { useAuth, isMember } from '@/contexts/AuthContext'
import { useToast } from '@/contexts/ToastContext'
@@ -190,6 +190,8 @@ export function BookDetailPage() {
const [deleting, setDeleting] = useState(false)
const [confirmDelete, setConfirmDelete] = useState(false)
const [downloadingId, setDownloadingId] = useState
+ The caret next to Read on a book page offers Preview: the + book opens in the same reader, but nothing is written back. Reading status, position, the + position synced to your devices, reading time and stats all stay as they were. Use it to + check a file's formatting or flip through a book before deciding to read it. A small + "Preview" tag in the reader bar shows it is on; it applies to that one open only. + Highlights and notes still work. +