Turn any long webpage into a chapter of a book.
Paginated like an ebook. Remembers the sentence you stopped on. Works on a plane. No account, no server, nothing leaves your device.
Long-form reading in a browser is a scroll bar and a lot of furniture. Bookify takes the article out of the page and gives it back to you as pages: a fixed text block, explicit page-turn buttons, a table of contents, and a reading position that survives everything a phone can do to a layout.
It is a Progressive Web App. You install it to your Home Screen, and after that it launches full screen and reads offline, because the articles live in your browser's own database rather than on a server.
Two pieces make it work:
- the app — your library and the reader
- a bookmarklet — one bookmark that captures whatever you are reading
A web app cannot look inside another Safari tab. The bookmarklet is what crosses that gap: you run it on the article you are reading, and it either paginates the page in place or hands it to your library.
| Pages, not scrolling | CSS multi-column layout, instant page turns, no animation |
| Never loses your place | position is a text anchor, not a page number |
| Technical articles | code blocks, wide scrollable tables, figures, inline SVG, MathML |
| A real dark mode | reader-owned colours; an imported stylesheet can't make text invisible |
| Offline | app shell and articles are cached; airplane mode is a supported way to read |
| Yours | IndexedDB on your device, with JSON export whenever you want it |
| Shareable | send a passage; it opens in their Bookify at the same sentence |
- Open jamesdavid.github.io/bookify in Safari.
- Tap the Share button.
- Choose Add to Home Screen.
- Tap Add.
Bookify now has an icon like any other app, launches without browser chrome, and works with no connection.
On a desktop browser, use the install button in the address bar, or Settings → Install Bookify inside the app.
Tap Add the sample chapter the first time you open Bookify. It puts a real technical chapter in your library — prose, code, a wide table, a diagram — so every part of the reader has something to show you.
Each row shows the source, the reading time, when you last read it, and how far in you are. The … button on the right opens per-article actions: mark finished, mark unread, open the original page, delete.
Sort the shelf with the control at the top right: recently read, recently added, title, or progress.
The reader is deliberately plain: a title bar, the text, and a page strip. The article opens with its title, the way a chapter does in a book.
Pages turn when you press ‹ or ›. Nothing else turns a page.
That is a deliberate design decision rather than an omission. Swiping, dragging, tapping the edges and pressing arrow keys all do nothing, which is what makes the next part possible:
A table wider than the screen scrolls inside its own box. Drag it sideways and the table moves; the page does not. The same is true of code blocks set to scroll, and of any image you want to explore with two fingers. In a reader that turns pages on swipe, inspecting a wide table means losing your place.
A- and A+ in the title bar change the text size. The page count changes with it — and the sentence you were reading stays on screen. Bookify stores where you are in the text, not which page you were on, so nothing is lost when the layout changes.
Contents (≡ on a phone) lists the article's headings with their page numbers and marks the section you are in. Choosing one jumps straight to the page holding that heading.
Aa opens typeface, margins, line spacing and code wrapping, along with mark finished, open original, and the article's word count.
Preferences are global — set them once and every article follows.
Select a sentence and Share passage appears above the page strip. There is also a share control in the title bar, which shares the whole article.
Bookify has no server, so a shared link cannot carry the article itself. It carries what is needed to find it: the page it came from, the title, and the passage. What your recipient gets depends on what they have:
- They have the article — it opens in their reader at that passage.
- They don't — they see the quote and a button to open the original, which their own bookmarklet can capture. The same link then works properly.
- They don't have Bookify — the page explains itself and offers the original article.
Bookify follows your system by default; Settings → Theme overrides it.
Dark mode is authored rather than inverted. Imported articles arrive with their stylesheets stripped, so the reader's own colours decide everything: body text, captions, code, table borders and links are all guaranteed readable in both themes. (An automated test measures the contrast of each one.)
This is what lets you capture the page you are actually reading.
Open Bookmarklet in Bookify's top bar, then:
On a desktop browser
- Show your bookmarks bar (
Ctrl/Cmd+Shift+B). - Drag the Bookify button onto it.
On iPhone or iPad
- Tap Copy bookmarklet.
- In Safari, tap Share → Add Bookmark, name it
Bookify, and save. - Open Bookmarks, tap Edit, and select the new Bookify bookmark.
- Select its address, delete it, paste what you copied, and tap Done.
Step 4 is fiddly, and it is worth doing carefully: the address must be replaced entirely, not appended to.
The bookmarklet is self-contained — it carries its own reader and needs no network once a page is open. It is pure ASCII, so Safari stores it in a bookmark without mangling it. (Earlier versions of this idea produced
×and‹in the buttons; the build now fails if a single non-ASCII character reaches the output.)If you would rather have a tiny bookmark that always runs the latest version, the install page also offers a loader variant. It needs a connection.
Open a long article, then tap your Bookify bookmark.
Bookify pulls the article out of the page — dropping the navigation, the share buttons, the sidebar, the newsletter box and the cookie banner — and offers two things.
Paginates the article in place, over the page you are on. Same reader, same rules: explicit page buttons only, scrollable tables, dark mode, A-/A+. It remembers your place per article, so you can come back to it later.
Tap your Bookify bookmark again to close it. Esc also closes it on a desktop.
Adds the article to your library, where it works offline and syncs its position with everything else you are reading.
Bookify opens in a new tab and the article is handed over directly. If that does not work — a blocked popup, an unusual Safari configuration — the article is copied to your clipboard instead, and Bookify tells you so. Open the app, go to Import, and choose Import from clipboard.
An article is never quietly lost. Every import attempt is recorded, and every failure says what went wrong in words you can act on.
Saving the same article twice updates it in place. Your reading position and notes are kept.
Two different pages are never merged, even when the site says they are the same. Some sites — book and documentation sites especially — publish one site-wide
og:urlandog:titleon every page. Bookify ignores a canonical URL that points at the site root or at one of the page's own parent directories, and refuses to overwrite a stored article unless the incoming content actually matches it.
This one catches everybody, and it is worth understanding before you build up a library in the wrong place.
iOS gives a Home Screen web app its own storage container. Cookies, localStorage and IndexedDB are not shared with Safari, even though it is the same site at the same address. So the articles you saved in Safari are simply not in the installed app, and vice versa. No web API can bridge that.
It has a consequence that matters more than the first surprise: the bookmarklet runs in Safari, so everything it saves lands in Safari's library. The installed app will never fill up on its own.
So pick where you read:
- Read in Safari. The bookmarklet saves straight into it. You give up the full-screen app and the Home Screen icon.
- Read in the installed app. Nicer to read in, launches offline — but you capture in Safari and move articles across when you want them.
To move them: in the copy that has the articles, open Settings → Safari and the Home Screen → Send this library across. Then open the other copy and choose Bring in a library.
It works because the one thing iOS does share between the two is the Cache API. If that fails, the same section will copy your whole library to the clipboard so you can paste it into the other side.
Moving a library merges rather than overwrites. An article is only replaced by a newer copy of itself, and reading progress always keeps whichever side got further — so you can move it back and forth without losing your place.
Nothing special to do. Save what you want to read, then put the device in airplane mode: Bookify launches from the Home Screen, your library is there, and articles open exactly where you left them.
Images from saved articles are cached too, so a chapter still looks like itself with no connection.
Everything Bookify stores is on your device, which means nobody else can lose it and nobody else can look at it. It also means clearing your browser's site data takes it with it.
Settings → Export library JSON writes a single file containing every article, position and note. Import library JSON reads it back — on the same device or a new one.
npm install
npm run dev # http://localhost:5173/bookify/The dev server does not register the service worker; use a production build to exercise offline behaviour.
npm test # unit tests (Vitest, jsdom)
npm run test:e2e # end-to-end tests (Playwright, desktop + phone)The end-to-end suite builds the app and runs against the real production output — it needs the service worker and the built bookmarklet to exist. First run:
npx playwright install chromiumWhat the suites cover:
- unit — sanitization, URL normalization, text indexing and offset mapping, quote anchors and the recovery ladder, extraction scoring and chrome removal, the import protocol, library sorting and formatting, the TOC
- end-to-end — pagination, only the buttons turn pages, wide-table scrolling, font resize, viewport change, close/reopen, contents, dark-mode contrast ratios, offline launch and offline reading, the bookmarklet on a page with real site chrome, glyph code points, and the import protocol including its failure messages
npm run build # typecheck, bundle, bookmarklet, service worker
npm run previewThe build produces:
| Output | What it is |
|---|---|
dist/ |
the static site |
dist/sw.js |
service worker, with the precache list injected from the real build output |
dist/bookmarklet.txt |
the self-contained bookmarklet, asserted ASCII-safe |
dist/bookmarklet-loader.txt |
the small network-loading variant |
dist/test-article.html |
the end-to-end fixture |
Other scripts:
npm run icons # regenerate app icons (procedural, no design tooling)
npm run fixture # regenerate the test fixture page
node scripts/capture-screenshots.mjs # regenerate the screenshots in this READMEPushing to main runs .github/workflows/deploy-pages.yml, which typechecks,
runs both test suites, builds, and deploys. To set it up on a fork:
- Settings → Pages → Build and deployment → Source: choose GitHub Actions.
- Push to
main. - The site appears at
https://<user>.github.io/<repo>/.
The base path comes from the repository name (BOOKIFY_BASE), so nothing
assumes it is served from a domain root. To host elsewhere:
BOOKIFY_BASE=/ BOOKIFY_ORIGIN=https://example.com npm run buildBOOKIFY_ORIGIN is baked into the bookmarklet: it is the only origin the
bookmarklet will send an article to, and the only one it accepts a reply from.
Rebuild the bookmarklet whenever you change where the app is hosted.
any webpage in the browser
|
| bookmarklet
v
+--------------------+
| extractor | scores candidate containers, strips site chrome,
+--------------------+ sanitizes, absolutizes URLs
|
+-------+--------+
| |
Read Here Save to Bookify
| |
v | Bookify Import Protocol (versioned, transport-agnostic)
overlay reader | postMessage to a known origin
(shadow DOM) | clipboard fallback
| v
| +-----------+
| | Bookify |
| | PWA |
| +-----------+
| |
| v
| +-----------+
| | IndexedDB | articles, positions, annotations,
| +-----------+ preferences, import log
| |
+--------+-------+
v
pagination engine CSS columns, semantic position anchors
(shared by both)
The pieces are deliberately separable:
- extraction knows nothing about transport or UI, so a Safari Web Extension could replace the bookmarklet without touching anything else
- the import protocol is a versioned envelope any producer can emit — bookmarklet, extension, desktop tool, server fetcher
- the pagination engine and the position code are shared by the app and the bookmarklet, so there is one definition of "where the reader is"
- storage is behind a small API with an explicit schema version, and a
SyncProviderseam exists for a future backend. v1 ships local-only.
Page numbers are worthless as a bookmark: they change with font size, margins, window size and orientation. Bookify stores a text quote anchor against the article's normalized text — roughly 90 characters of exact text with 40 characters of context on either side — plus a character offset, the enclosing heading, and a normalized percentage.
On every repagination it walks a recovery ladder:
- the exact quote, disambiguated by its prefix and suffix
- the character offset into the normalized text
- the nearest heading
- normalized percentage through the text
- the last known page number
Storage and the live reader run the same normalization function, so an offset computed when the article was saved still means the same thing months later, in a different font, on a different device.
src/
├── app.ts, router.ts, main.ts application shell and hash routing
├── types.ts the stored data model and the import protocol
├── library/ the shelf
├── reader/
│ ├── paginator.ts CSS column pagination
│ ├── position.ts text anchors and the recovery ladder
│ ├── content.ts runtime preparation of article markup
│ ├── typography.ts, toc.ts, sheet.ts
├── storage/ IndexedDB, with explicit migrations
├── import/ sanitize, normalize, protocol, importer
├── bookmarklet/ extractor, overlay reader, transport
├── pwa/ service worker and install
└── settings/ preferences, theme, backup
Imported HTML is treated as hostile. It is parsed with DOMParser (which never
executes scripts or fetches subresources) and filtered through an allowlist
of elements and attributes:
- no
script,iframe,object,embed,form, or form controls - no event-handler attributes, no inline
style javascript:,vbscript:,blob:andfile:URLs are dropped;data:is allowed only for images- relative URLs are resolved against the source page
- article links open externally with
rel="noopener noreferrer"
The import protocol validates every message's shape and version before anything
is stored, and postMessage always names an explicit origin — never "*".
- A PWA cannot read another browser tab. There is no web API for it. The bookmarklet performs capture, and that is why installing it is part of setup.
- Extraction is a heuristic. It scores candidate containers and prefers keeping too much over cutting a code listing in half. Some pages will still come out imperfect.
- The bookmarklet is a snapshot. The self-contained one carries its own copy of the extractor, so improvements to capture reach it only when you re-copy it from the Bookmarklet page. The loader variant updates itself.
- Paywalled or script-rendered content is captured as your browser rendered it. Bookify reads the page you are looking at; it does not fetch anything.
- No sync yet. Everything is local. Export JSON is the migration path, and the sync seam is in place for later.
- On iOS, Safari and the Home Screen app are two separate libraries. Apple gives an installed web app its own storage container, and the bookmarklet runs in Safari — so captures land there. Bookify can hand a library across (see the guide above), but it cannot keep them in step.
- iOS storage is not permanent. Bookify asks for persistent storage, which Safari grants after enough engagement. Export a backup before clearing site data.
- Highlights and notes have a schema and a store, but no authoring UI yet.
No accounts, no server, no subscription, no AI summaries, no social features, no crawling, no paywall circumvention. Bookify is a reader.
The original hand-written bookmarklet that inspired this project is preserved
verbatim in docs/prior-art/, together with a note on which
of its behaviours were carried forward and which were changed.
The full specification this was built from is in
docs/SPECIFICATION.md.
MIT











