@emtpzh/reader-component
v0.1.5
Published
Standalone reader extracted from Folo — publishable as a Web Component
Readme
@emtpzh/reader-component
A standalone article reader extracted from Folo, packaged as a Web Component (<folo-reader>).
Designed for use in browser extensions: the extension parses content and injects it via a ContentProvider, while the reader handles all rendering, dark mode, table of contents, and reading progress.
Build
# Standalone web app (dev/preview)
pnpm dev
pnpm build # → dist/
# Web Component npm package
pnpm build:wc # → dist-wc/folo-reader.jsUsage in a browser extension
1. Import the package
// Registers the <folo-reader> custom element as a side effect
import "@emtpzh/reader-component"Or load the built file directly:
<script type="module" src="folo-reader.js"></script>2. Implement a ContentProvider
import type { ContentProvider, ReaderEntry } from "@emtpzh/reader-component"
const provider: ContentProvider = {
// Return the list of entries shown in the sidebar
getEntries: async (): Promise<ReaderEntry[]> => {
return [
{
id: "1",
title: "My Article",
content: "<p>Hello world</p>",
publishedAt: new Date().toISOString(),
author: "Alice",
url: "https://example.com/article",
},
]
},
// Return full content for a single entry (called when user selects one)
getEntryDetail: async (id: string): Promise<ReaderEntry | null> => {
return myContentMap[id] ?? null
},
}3. Mount and inject the provider
import { setContentProvider } from "@emtpzh/reader-component"
// Inject provider before or after mounting — both work
setContentProvider(provider)
const reader = document.createElement("folo-reader")
reader.style.cssText = "width: 100%; height: 100vh;"
document.body.appendChild(reader)You can also call setProvider on the element instance directly:
const reader = document.querySelector("folo-reader") as any
reader.setProvider(provider)4. Update content dynamically
Call setContentProvider again at any time to swap the data source (e.g. when the user navigates to a new page).
setContentProvider(newProvider)ReaderEntry type
interface ReaderEntry {
id: string
title: string
content: string // HTML string
publishedAt: string // ISO 8601
url?: string
description?: string
author?: string
authorUrl?: string
authorAvatar?: string
categories?: string[]
media?: { url: string; type: "photo" | "video"; width?: number; height?: number }[]
attachments?: { url: string; title?: string; mime_type?: string; size_in_bytes?: number }[]
}CSS isolation
The component uses Shadow DOM. All Tailwind CSS is injected into the shadow root — no styles leak in or out. The host page's styles have no effect on the reader.
Dark mode follows prefers-color-scheme automatically and can be toggled by the user via the button in the bottom-right corner.
Externalizing React
By default, React is bundled into folo-reader.js (self-contained, no peer dependencies).
If the extension already bundles React 19, you can avoid shipping it twice by setting rollupOptions.external in vite.config.ts:
rollupOptions: {
external: ['react', 'react-dom'],
}Then make sure React is available in the extension's build graph before importing this package.
