Build Bookify, a lightweight installable PWA that turns long-form webpages into an ebook-like reading experience.
Bookify should allow a user to:
- Open a long webpage in Safari.
- Invoke a bookmarklet.
- Either:
- read the current webpage immediately in paginated mode, or
- save/import the article into the Bookify PWA.
- Read saved articles from a unified Bookify library.
- Resume at approximately the same sentence regardless of:
- font-size changes,
- viewport changes,
- device orientation,
- repagination.
- Read saved articles offline.
- Install Bookify to the iOS Home Screen as a standalone PWA.
The initial version must require no backend and must be deployable entirely using GitHub Pages.
The application should be designed so a backend sync service and Safari Web Extension can be added later without rewriting the reader or data model.
Bookify must distinguish between:
Getting readable content out of an arbitrary webpage already loaded in Safari.
Displaying that content in a persistent ebook-style library and reader.
The initial architecture is:
Safari webpage
|
| Bookify bookmarklet
|
+---------------------+
| |
v v
Read Here Save to Bookify
| |
Bookmarklet reader PWA import
|
v
IndexedDB
|
v
Bookify reader
Long term:
Bookmarklet
|
Safari Web Extension
|
+------ Bookify Import Protocol ------+
|
v
Bookify PWA
|
Sync API
The PWA reader and stored article format should therefore not depend on the bookmarklet implementation.
Initial priority:
- iPhone Safari / Home Screen PWA
- iPad Safari / Home Screen PWA
- macOS Safari
- Chrome/Edge/Firefox desktop
The reader should use standards-based HTML/CSS/JavaScript wherever possible.
Do not introduce native iOS code for v1.
Use:
TypeScript
Vite
HTML/CSS
IndexedDB
Service Worker
Web App Manifest
GitHub Actions
GitHub Pages
Prefer a lightweight architecture.
Do not use a heavy application framework unless it materially simplifies the project.
A small component framework is acceptable, but plain TypeScript or a lightweight library is preferred.
Suggested dependencies:
idb
vite
typescript
vitest
playwright
Optional:
dompurify
If DOMPurify is used, sanitize imported HTML before storing/rendering.
Avoid runtime CDN dependencies.
Everything required for the app should be bundled locally so the installed PWA works offline.
Suggested structure:
bookify/
├── README.md
├── package.json
├── tsconfig.json
├── vite.config.ts
├── index.html
├── public/
│ ├── manifest.webmanifest
│ ├── icons/
│ │ ├── icon-192.png
│ │ ├── icon-512.png
│ │ └── apple-touch-icon.png
│ └── favicon.ico
│
├── src/
│ ├── main.ts
│ ├── app.ts
│
│ ├── library/
│ │ ├── library.ts
│ │ ├── library-view.ts
│ │ └── library.css
│
│ ├── reader/
│ │ ├── reader.ts
│ │ ├── paginator.ts
│ │ ├── position.ts
│ │ ├── typography.ts
│ │ ├── reader.css
│ │ └── toc.ts
│
│ ├── storage/
│ │ ├── db.ts
│ │ ├── articles.ts
│ │ ├── positions.ts
│ │ ├── annotations.ts
│ │ └── preferences.ts
│
│ ├── import/
│ │ ├── importer.ts
│ │ ├── protocol.ts
│ │ ├── sanitize.ts
│ │ └── normalize.ts
│
│ ├── bookmarklet/
│ │ ├── bookmarklet.ts
│ │ ├── extractor.ts
│ │ ├── inline-reader.ts
│ │ └── build-bookmarklet.ts
│
│ ├── pwa/
│ │ ├── service-worker.ts
│ │ └── install.ts
│
│ ├── settings/
│ │ └── settings.ts
│
│ └── types.ts
│
├── tests/
│ ├── unit/
│ └── e2e/
│
└── .github/
└── workflows/
└── deploy-pages.yml
The PWA home screen should resemble a simple ebook library.
Example:
Bookify
Recently Read
Language models and the Transformer
Deep Learning with Python
28% complete
Last read 12 min ago
Attention Is All You Need
61% complete
Santos-Dumont
Finished
Each saved item should display:
- title,
- source/domain,
- reading progress,
- last-read date,
- optional estimated reading time.
Actions:
- open,
- delete,
- mark unread,
- mark finished,
- open original webpage.
Sort options:
- recently read,
- recently added,
- title,
- progress.
Initial release does not require folders or tags.
The reader should feel closer to Kindle/Apple Books than a web browser.
Standalone PWA:
+--------------------------------+
| x Article Title A- A+ |
+--------------------------------+
| |
| |
| paginated article |
| |
| |
+--------------------------------+
| < 36 / 129 > |
+--------------------------------+
The page-turn buttons are the only normal way to change pages.
DO NOT TURN PAGES WHEN THE USER:
- swipes horizontally,
- swipes vertically,
- taps the left side,
- taps the right side,
- taps the article background.
Page navigation occurs only when the explicit:
<
>
buttons are pressed.
Keyboard page navigation should also be disabled initially.
Escape may close the reader on desktop.
This requirement exists because:
- tables may require horizontal scrolling,
- code blocks may require scrolling,
- images or diagrams may be explored using touch gestures,
- accidental page turns are disruptive.
Use CSS multi-column layout.
Conceptually:
.reader-content {
height: var(--page-height);
column-width: var(--page-width);
column-gap: var(--page-gap);
column-fill: auto;
}The reader viewport must clip the complete column set.
Changing pages should update the content offset immediately.
Avoid animated scrolling by default.
Especially on e-ink-like devices and mobile browsers:
instant page change
is preferable to:
smooth horizontal animation
The paginator must calculate:
pageWidth
columnGap
columnStride
totalPages
currentPage
Do not assume:
pageStride === window.innerWidth
because margins and gaps affect layout.
Repaginate when:
- viewport changes,
- device rotates,
- font size changes,
- line-height changes,
- margins change,
- fonts finish loading,
- an image affecting layout finishes loading.
Repagination must restore the user's semantic reading position, not merely the previous numeric page.
Example:
Before font-size change:
Page 36 / 129
After font-size increase:
Page 47 / 168
The visible text should still begin at approximately the same sentence.
Do not treat page number as the authoritative reading position.
Pages are ephemeral.
Store a semantic anchor.
Suggested representation:
interface ReadingPosition {
articleId: string;
progress: number;
quote?: {
exact: string;
prefix?: string;
suffix?: string;
};
textOffset?: number;
headingId?: string;
lastKnownPage?: number;
lastKnownTotalPages?: number;
updatedAt: number;
}Primary recovery order:
1. exact quote + prefix/suffix
2. text offset
3. heading + local offset
4. normalized percentage
5. previous page number
The goal is to survive:
- typography changes,
- screen changes,
- moderate source HTML changes.
When saving reading position, determine a text range near the beginning of the visible page.
Store approximately:
prefix: 20–50 chars
exact: 40–120 chars
suffix: 20–50 chars
Example:
{
"prefix": "representation of the source sequence. We can achieve this with a ",
"exact": "Bidirectional layer.",
"suffix": " Next, let's add the decoder"
}Normalize:
- repeated whitespace,
- non-breaking spaces,
- line breaks.
On restoration:
- generate normalized article text,
- search for
exact, - disambiguate using prefix/suffix,
- map match back to DOM,
- find the generated CSS column containing it,
- navigate to that column.
Use a versioned stored representation.
interface Article {
id: string;
schemaVersion: number;
title: string;
author?: string;
sourceUrl: string;
canonicalUrl?: string;
sourceDomain: string;
html: string;
plainText: string;
headings: ArticleHeading[];
savedAt: number;
updatedAt: number;
lastReadAt?: number;
estimatedMinutes?: number;
}Use a deterministic ID when practical.
For example:
SHA-256(canonical URL)
or use a UUID plus a canonical URL uniqueness index.
Do not create duplicate copies every time an article is saved.
If the same canonical URL already exists, prompt/update appropriately.
Use IndexedDB rather than localStorage for application content.
Suggested stores:
articles
positions
annotations
preferences
imports
Possible schema:
articles
key: id
indexes:
canonicalUrl
savedAt
lastReadAt
positions
key: articleId
annotations
key: annotationId
index:
articleId
preferences
key: name
Use a schema version and explicit migrations.
Persist globally:
interface ReaderPreferences {
fontSize: number;
lineHeight: number;
marginSize:
| "small"
| "medium"
| "large";
theme:
| "system"
| "light"
| "dark";
font:
| "serif"
| "sans";
}Initial UI only requires:
A-
A+
but design the data model for future settings.
Dark mode must be intentionally styled.
Do not rely on arbitrary source-page CSS.
Reader-owned CSS should override imported colors where necessary.
Suggested variables:
--reader-bg
--reader-fg
--reader-muted
--reader-panel
--reader-border
--reader-link
--reader-code
--reader-inline-codeIn dark mode:
- body text must be light,
- captions must remain readable,
- code-block text must not remain black,
- table borders must remain visible,
- links must remain distinguishable,
- inline code may use a contrasting accent.
Imported source styles should not be allowed to make text invisible.
SVG handling may require a white background where the source assumes black-on-white rendering.
Code blocks must remain usable.
Default behavior may wrap code where practical:
white-space: pre-wrap;
overflow-wrap: anywhere;However, preserve semantic code formatting.
Later configuration may allow:
Wrap code: ON/OFF
If horizontal scrolling is used, scrolling the code must never trigger page navigation.
This requirement is important.
Wide tables must be independently horizontally scrollable.
Wrap tables as:
<div class="bookify-table-scroll">
<table>...</table>
</div>Use:
.bookify-table-scroll {
width: 100%;
max-width: 100%;
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
touch-action: pan-x;
}The table itself may use:
width: max-content;
min-width: 100%;A horizontal finger drag over a table must scroll the table.
It must never turn the ebook page.
Images should:
- scale down to page width,
- preserve aspect ratio,
- avoid splitting across pages when possible.
Use:
break-inside: avoid;
max-width: 100%;
height: auto;Figures and captions should generally remain together.
Large images should remain viewable without forcing article width beyond the page.
Future enhancement:
tap image -> fullscreen image viewer
Not required for v1.
Generate a TOC from:
h1
h2
h3
Normalize missing IDs.
Example:
Chapter 15
Sequence-to-sequence learning
RNN-based sequence models
Transformers
Self-attention
Multi-head attention
Selecting a heading should:
- find the DOM heading,
- determine the containing CSS column,
- jump directly to that page.
Maintain the existing ability to run Bookify directly inside an arbitrary webpage.
The bookmarklet should present:
Bookify
[ Read Here ]
[ Save to Bookify ]
The exact menu styling may evolve.
Read Here should recreate the existing Bookify behavior:
- overlay reader,
- CSS pagination,
- dark mode,
- A-/A+ controls,
- page count,
- local reading position,
- horizontal table scrolling,
- explicit page-turn buttons.
Do not include:
- swipe page turning,
- tap-edge page turning.
Only the previous/next buttons turn pages.
The bookmarklet URL itself should be generated in an ASCII-safe form.
Previous prototypes exposed mojibake such as:
×
‹
›
when Unicode UI symbols were stored inside Safari bookmark URLs.
Avoid literal non-ASCII punctuation inside bookmarklet source.
Use either:
×
‹
›or plain ASCII equivalents.
The bookmarklet build should include an automated assertion:
if (!bookmarkletIsAscii) {
throw new Error(...)
}Maintain readable source:
src/bookmarklet/bookmarklet.ts
Generate:
dist/bookmarklet.txt
The output should be:
javascript:(()=>{...})();
minified to one line.
Provide a development page in Bookify that displays:
Install Bookmarklet
1. Bookmark this page.
2. Edit the bookmark.
3. Replace its URL with:
[Copy Bookmarklet]
Include a one-tap copy button where browser permissions permit.
The bookmarklet must determine the primary content area.
Candidate order:
article
main
[role="main"]
known content containers
body fallback
Use a scoring heuristic if necessary.
Prefer semantic content containing substantial:
p
h1
h2
h3
pre
code
figure
table
img
Remove obvious:
nav
footer
aside
cookie banners
advertising
share widgets
site headers
Do not aggressively remove legitimate technical content.
The Deep Learning with Python chapter should be included as a regression fixture/test case because it contains:
- prose,
- headings,
- code,
- captions,
- inline code,
- links.
Never execute scripts from imported articles.
Remove:
script
iframe
object
embed
form
input
button
textarea
unless explicitly required later.
Strip:
onload
onclick
onerror
...
and other event-handler attributes.
Do not preserve arbitrary executable JavaScript.
Sanitize URLs.
The reader should display imported content as inert document content.
Resolve relative resource URLs before storing.
Given:
<img src="../images/foo.png">convert it to an absolute URL based on the source page.
Do the same for:
img src
source src
a href
Preserve original article links.
Clicking a normal article link should open Safari/the external browser rather than navigate the PWA away from its application shell.
Define a versioned protocol independent of the bookmarklet.
Example:
interface BookifyImportMessage {
protocol: "bookify-import";
version: 1;
article: {
sourceUrl: string;
canonicalUrl?: string;
title: string;
author?: string;
html: string;
plainText?: string;
};
}This same format should eventually be usable by:
- bookmarklets,
- Safari extension,
- Chrome extension,
- desktop utility,
- backend fetcher.
Implement transport defensively.
Preferred automatic path:
bookmarklet
|
window.open(Bookify import page)
|
v
PWA loads
|
BOOKIFY_READY
|
v
bookmarklet sends import payload via postMessage
Validate:
origin
protocol
version
payload structure
Do not use postMessage(..., "*") for the final implementation.
The expected Bookify origin should be configured at build time.
Mobile Safari behavior around opened windows should be treated as something that must be empirically tested.
Therefore provide a fallback path.
Suggested fallback:
- serialize the Bookify import payload,
- copy it to clipboard,
- open Bookify
/import, - provide an
Import from Clipboardbutton.
Do not depend on extremely long query strings or URL fragments for full article HTML.
The application should present a useful error instead of silently losing an article.
Configure an installable web application.
Example behavior:
{
"name": "Bookify",
"short_name": "Bookify",
"display": "standalone",
"start_url": "./",
"scope": "./"
}Be careful about GitHub Pages subdirectory hosting.
The app may initially be deployed at:
https://USERNAME.github.io/bookify/
All paths must work under /bookify/.
Do not hardcode root-relative paths that assume /.
Use Vite's configured base path.
Deployment must work through GitHub Actions.
A push to:
main
should:
- install dependencies,
- run lint,
- run unit tests,
- build production assets,
- deploy
dist/to GitHub Pages.
README should document:
Settings
-> Pages
-> GitHub Actions
where applicable.
The service worker should cache the application shell.
At minimum:
index.html
JS bundles
CSS
icons
manifest
Saved article HTML lives in IndexedDB rather than requiring service-worker URL caching.
The installed application should launch while offline.
This flow must work:
1. User opens Bookify online.
2. User saves an article.
3. User reads several pages.
4. User closes Bookify.
5. Device is put into airplane mode.
6. User launches Bookify from Home Screen.
7. Saved article appears.
8. User opens it.
9. Reader resumes near the previous location.
10. User continues reading.
Calculate progress semantically rather than simply:
current page / total pages
when practical.
The page count changes with typography.
Store normalized text progress:
character offset / total normalized characters
or equivalent.
The UI may display:
36 / 129
for the current layout while the library displays:
28%
for persistent progress.
When importing an article whose canonical URL already exists:
Default behavior:
Update saved article
while preserving:
- reading position,
- annotations,
- date first saved.
If new text is substantially different, preserve old annotations as best possible via quote anchors.
A sophisticated diff engine is not required for v1.
Design the schema now but implementation may be milestone 2.
Suggested representation:
interface Annotation {
id: string;
articleId: string;
type:
| "highlight"
| "bookmark"
| "note";
quote: TextQuoteSelector;
note?: string;
createdAt: number;
updatedAt: number;
}Use quote selectors rather than DOM XPath-only anchors.
Milestone 2 feature.
Search locally across:
title
domain
plain article text
notes
No backend required.
Future-friendly design.
Eventually support:
Markdown
HTML
EPUB
JSON backup
For v1, provide at minimum:
Export Library JSON
Import Library JSON
This gives the user a backup mechanism before cloud sync exists.
The production PWA must be a fully static GitHub Pages deployment.
Do not add:
Node server
Django
Firebase
Supabase
Cloudflare Workers
AWS
database server
login/authentication
for v1.
All state is local.
The application architecture should make later sync possible.
Create an abstraction such as:
interface SyncProvider {
push(...): Promise<void>;
pull(...): Promise<void>;
}but implement:
LocalOnlySyncProvider
for now.
Do not prematurely implement network synchronization.
The bookmarklet extractor/import protocol must be separated from UI code so a future Safari Web Extension can do:
rendered webpage
|
extension content script
|
Bookify import payload
|
PWA/local database
without changing article storage or reader behavior.
Use real buttons.
Controls must include appropriate:
aria-label
values.
Respect system text rendering.
Use adequate contrast in:
light
dark
themes.
Do not make essential functionality gesture-only.
The explicit page-turn buttons should have large touch targets.
Target smooth operation on long technical articles.
Do not rerun expensive full-document calculations on every touch event.
Debounce:
resize
orientation changes
font changes
Repagination should generally complete within a fraction of a second for ordinary articles.
Articles containing very large DOM trees should remain usable.
Do not prevent ordinary horizontal touch interaction inside:
tables
code scrollers
images/viewers
Do not globally intercept touch events except where strictly necessary.
The reader page itself should not use swipe navigation.
Create test fixtures covering:
Multiple paragraphs/headings.
Long code block.
Table wider than viewport.
Verify horizontal scrolling does not turn page.
Verify:
body text visible
code visible
caption visible
links visible
Large image and caption.
Repaginate and restore semantic position.
Increase/decrease font while staying at approximately same sentence.
Close/open article and restore position.
Open stored content with network disabled.
Article text may contain Unicode.
Bookmarklet itself must remain ASCII-safe.
Include a local development fixture:
/test-article.html
containing:
- 20+ paragraphs,
- h1/h2/h3,
- long code,
- wide table,
- image,
- links,
- inline code,
- blockquote,
- list,
- Unicode punctuation.
Use it for automated browser testing.
README should include:
Short explanation.
npm install
npm run devnpm test
npm run test:e2enpm run buildExact steps.
Safari
Share
Add to Home Screen
Exact instructions.
Diagram explaining:
Bookmarklet -> Import Protocol -> PWA -> IndexedDB
Explain that a PWA cannot directly inspect arbitrary Safari tabs and that the bookmarklet currently performs capture.
Implement:
- Vite/TypeScript app,
- GitHub Pages configuration,
- manifest,
- service worker,
- installable PWA shell,
- basic README.
Acceptance:
npm run build
works and app deploys to GitHub Pages.
Commit:
milestone-1-pwa-scaffold
Implement:
- database schema,
- articles,
- positions,
- preferences,
- sample article import,
- library screen.
Acceptance:
Saved sample articles survive browser reload.
Commit:
milestone-2-local-library
Implement:
- CSS columns,
- page count,
- previous/next buttons,
- reader chrome,
- font resizing,
- light/dark mode.
Explicit requirement:
NO SWIPE PAGE TURNING
NO TAP-EDGE PAGE TURNING
Acceptance:
Only the page buttons change pages.
Commit:
milestone-3-reader-pagination
Implement proper handling for:
- code,
- inline code,
- tables,
- figures,
- captions,
- links,
- SVG,
- dark mode.
Acceptance:
Wide table can be finger-scrolled left/right without page navigation.
Commit:
milestone-4-technical-content
Implement:
- normalized article text,
- quote anchors,
- position persistence,
- repagination restoration.
Acceptance:
User opens an article, navigates into it, enlarges font, rotates device, closes/reopens article, and returns to approximately the same sentence.
Commit:
milestone-5-semantic-position
Implement:
- article extraction,
- current-page overlay reader,
- ASCII-safe build,
- explicit page buttons,
- table scrolling.
Acceptance:
Bookmarklet works on the Deep Learning with Python chapter and does not produce mojibake.
Commit:
milestone-6-bookmarklet-reader
Implement:
- import protocol,
- bookmarklet article extraction,
- PWA import receiver,
postMessagehandshake,- origin verification,
- fallback clipboard import.
Acceptance:
User can invoke bookmarklet on an external page and add the article to Bookify.
Commit:
milestone-7-bookmarklet-import
Implement:
- app shell caching,
- reliable offline startup,
- saved article availability offline.
Acceptance:
Complete airplane-mode test succeeds.
Commit:
milestone-8-offline
Implement:
- generated TOC,
- progress display,
- recently-read ordering,
- delete confirmation,
- open original link,
- responsive iPhone/iPad layout.
Commit:
milestone-9-reader-polish
Implement:
- Vitest unit tests,
- Playwright E2E tests,
- fixture article,
- GitHub Actions test/build/deploy,
- complete README.
Commit:
milestone-10-release
V1 is complete when the following works on an iPhone:
1. Visit Bookify on GitHub Pages.
2. Add Bookify to Home Screen.
3. Install Bookify bookmarklet.
4. Visit a long technical webpage in Safari.
5. Invoke Bookify.
6. Choose Save to Bookify.
7. Article appears in Bookify library.
8. Open article.
9. Article is ebook-paginated.
10. Press > to advance one page.
11. Swipe article horizontally: page does NOT change.
12. Swipe a wide table horizontally: table scrolls.
13. Increase font size: approximately same sentence remains visible.
14. Close Bookify.
15. Reopen Bookify.
16. Article resumes near same sentence.
17. Put device into airplane mode.
18. Reopen Bookify.
19. Saved article remains readable.
Do not implement:
- cloud accounts,
- server database,
- subscription/billing,
- AI summarization,
- social features,
- automatic web crawling,
- paywall bypass,
- Safari native extension,
- App Store application,
- Android share target,
- multi-user collaboration.
Keep v1 focused.
Bookify should feel like:
ebook reader
+
read-it-later application
+
browser reader mode
not like a conventional website.
Design characteristics:
- restrained,
- typography-first,
- large reading area,
- minimal chrome,
- excellent dark mode,
- no distracting animations,
- no card-heavy dashboard aesthetic,
- no unnecessary gradients,
- no excessive icons.
The article content should dominate the experience.
Use small modules and explicit interfaces.
Avoid a single massive reader script.
Separate:
article extraction
article sanitization
storage
pagination
position tracking
UI
bookmarklet transport
Maintain strict TypeScript types.
Do not silently swallow failures.
Provide useful development logging behind a debug flag.
Production UI should show meaningful errors such as:
Bookify couldn't identify the main article on this page.
or:
The article was copied. Open Bookify and choose Import from Clipboard.
Prefer simple browser primitives over recreating browser functionality.
Examples:
Use:
CSS columns
rather than writing a custom line-layout engine.
Use:
IndexedDB
rather than inventing storage.
Use:
service worker
rather than requiring a server.
Use:
semantic text anchors
rather than treating page numbers as permanent identifiers.
The browser is already an excellent HTML typesetter. Bookify should provide the missing:
pagination
navigation
persistence
library
offline layer
around it.
Implement this specification incrementally.
Work milestone-by-milestone in the order given.
For each milestone:
- inspect the existing repository before modifying anything;
- make the smallest coherent implementation satisfying that milestone;
- run applicable tests;
- fix failures before proceeding;
- update
README.mdwhen behavior or setup changes; - commit the milestone using a descriptive commit message;
- do not move to the next milestone until the current milestone builds successfully.
Do not collapse the project into one large implementation pass.
Maintain working software at every milestone.
If an architectural decision differs from this specification because of a demonstrated browser limitation, document:
the limitation,
the alternatives considered,
the implemented workaround
in:
docs/architecture-decisions.md
The highest-priority behavioral requirements are:
- Bookify must work as a static GitHub Pages PWA.
- Saved articles must work offline.
- Reading position must survive repagination.
- Page turns must occur only from explicit page-turn buttons.
- Horizontal table scrolling must never change the ebook page.
- The bookmarklet must remain ASCII-safe.
- Imported pages must not execute source JavaScript.
- The storage/import format must remain independent of the capture mechanism.
Build a functional product, not merely a visual mockup.