style(public): overhaul landing and privacy pages with new layout, hero, and theme effects
Redesign the public landing and privacy pages with a new layout structure, introducing an aurora background, grain overlay, and updated navigation. Revise hero section with animated word-level highlighting and refreshed copy. Update CSS to use semantic theme variables, color-mix, and new background effects. Adjust navigation, footer, and action button order for clarity. Update privacy page button label for consistency.
This commit is contained in:
parent
14807a0e8c
commit
535d562a0b
4 changed files with 1259 additions and 626 deletions
|
|
@ -10,7 +10,9 @@ export default async function PublicLayout({ children }: { children: ReactNode }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="public-shell">
|
<div className="public-shell">
|
||||||
<div className="public-atmosphere" aria-hidden="true" />
|
<div className="public-aurora" aria-hidden="true" />
|
||||||
|
<div className="public-grain" aria-hidden="true" />
|
||||||
|
|
||||||
<div className="public-frame">
|
<div className="public-frame">
|
||||||
<div className="public-topbar">
|
<div className="public-topbar">
|
||||||
<div className="public-wrap">
|
<div className="public-wrap">
|
||||||
|
|
@ -18,30 +20,34 @@ export default async function PublicLayout({ children }: { children: ReactNode }
|
||||||
<Link href="/" className="public-brand" aria-label="OpenReader home">
|
<Link href="/" className="public-brand" aria-label="OpenReader home">
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
<img src="/icon.svg" alt="" className="public-brand-mark" aria-hidden="true" />
|
<img src="/icon.svg" alt="" className="public-brand-mark" aria-hidden="true" />
|
||||||
<span>
|
<span className="public-brand-copy">
|
||||||
<span className="public-brand-text">OpenReader</span>
|
<span className="public-brand-text">OpenReader</span>
|
||||||
<span className="public-brand-tag">Read + Listen</span>
|
<span className="public-brand-tag">Read · Listen</span>
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<nav className="public-nav" aria-label="Primary">
|
<nav className="public-nav" aria-label="Primary">
|
||||||
<Link href="/app" className={buttonClass({ variant: 'primary', size: 'sm' })}>
|
|
||||||
Open App
|
|
||||||
</Link>
|
|
||||||
<Link href="/signin" className={buttonClass({ variant: 'secondary', size: 'sm' })}>
|
|
||||||
Sign In
|
|
||||||
</Link>
|
|
||||||
{enableUserSignups ? (
|
|
||||||
<Link href="/signup" className={buttonClass({ variant: 'outline', size: 'sm' })}>
|
|
||||||
Sign Up
|
|
||||||
</Link>
|
|
||||||
) : null}
|
|
||||||
<Link
|
<Link
|
||||||
href="https://docs.openreader.richardr.dev/"
|
href="https://docs.openreader.richardr.dev/"
|
||||||
className={buttonClass({ variant: 'ghost', size: 'sm' })}
|
className="public-nav-link"
|
||||||
>
|
>
|
||||||
Docs
|
Docs
|
||||||
</Link>
|
</Link>
|
||||||
|
<a
|
||||||
|
href="https://github.com/richardr1126/openreader#readme"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="public-nav-link"
|
||||||
|
>
|
||||||
|
GitHub
|
||||||
|
</a>
|
||||||
|
<span className="public-nav-divider" aria-hidden="true" />
|
||||||
|
<Link href="/signin" className={buttonClass({ variant: 'ghost', size: 'sm' })}>
|
||||||
|
Sign in
|
||||||
|
</Link>
|
||||||
|
<Link href="/app" className={buttonClass({ variant: 'primary', size: 'sm' })}>
|
||||||
|
Open app
|
||||||
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -52,19 +58,67 @@ export default async function PublicLayout({ children }: { children: ReactNode }
|
||||||
<footer className="public-footer">
|
<footer className="public-footer">
|
||||||
<div className="public-wrap">
|
<div className="public-wrap">
|
||||||
<div className="public-footer-inner">
|
<div className="public-footer-inner">
|
||||||
<p className="public-footer-label">Open source document reader with synchronized text-to-speech playback.</p>
|
<div className="public-footer-brand">
|
||||||
<div className="public-footer-links">
|
<div className="public-footer-mark">
|
||||||
<Link href="/privacy">Privacy</Link>
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
<a href="https://github.com/richardr1126/openreader#readme" target="_blank" rel="noopener noreferrer">
|
<img src="/icon.svg" alt="" className="public-brand-mark" aria-hidden="true" />
|
||||||
GitHub
|
<span className="public-brand-text">OpenReader</span>
|
||||||
</a>
|
</div>
|
||||||
<a href="https://github.com/richardr1126/openreader/discussions" target="_blank" rel="noopener noreferrer">
|
<p className="public-footer-label">
|
||||||
Discussions
|
An open-source reading room that turns documents into
|
||||||
</a>
|
synchronized, listenable audio that’s yours to self-host.
|
||||||
<a href="https://docs.openreader.richardr.dev/" target="_blank" rel="noopener noreferrer">
|
</p>
|
||||||
Documentation
|
<div className="public-footer-cta">
|
||||||
</a>
|
{enableUserSignups ? (
|
||||||
|
<Link href="/signup" className={buttonClass({ variant: 'outline', size: 'sm' })}>
|
||||||
|
Create account
|
||||||
|
</Link>
|
||||||
|
) : null}
|
||||||
|
<a
|
||||||
|
href="https://github.com/richardr1126/openreader"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className={buttonClass({ variant: 'ghost', size: 'sm' })}
|
||||||
|
>
|
||||||
|
Star on GitHub
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<nav className="public-footer-cols" aria-label="Footer">
|
||||||
|
<div className="public-footer-col">
|
||||||
|
<p className="public-footer-col-title">Product</p>
|
||||||
|
<Link href="/app">Open app</Link>
|
||||||
|
<Link href="/signin">Sign in</Link>
|
||||||
|
<a href="https://docs.openreader.richardr.dev/" target="_blank" rel="noopener noreferrer">
|
||||||
|
Documentation
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="public-footer-col">
|
||||||
|
<p className="public-footer-col-title">Project</p>
|
||||||
|
<a href="https://github.com/richardr1126/openreader#readme" target="_blank" rel="noopener noreferrer">
|
||||||
|
GitHub
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/richardr1126/openreader/discussions" target="_blank" rel="noopener noreferrer">
|
||||||
|
Discussions
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/richardr1126/openreader/issues" target="_blank" rel="noopener noreferrer">
|
||||||
|
Issues
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="public-footer-col">
|
||||||
|
<p className="public-footer-col-title">Legal</p>
|
||||||
|
<Link href="/privacy">Privacy & data</Link>
|
||||||
|
<a href="https://github.com/richardr1126/openreader/blob/main/LICENSE" target="_blank" rel="noopener noreferrer">
|
||||||
|
MIT license
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="public-footer-base">
|
||||||
|
<div className="prism-divider" />
|
||||||
|
<p>© {new Date().getFullYear()} OpenReader · MIT licensed · Self-host friendly</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import type { CSSProperties } from 'react';
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { getResolvedRuntimeConfigForRsc } from '@/lib/server/runtime-config-rsc';
|
import { getResolvedRuntimeConfigForRsc } from '@/lib/server/runtime-config-rsc';
|
||||||
|
|
@ -42,164 +43,326 @@ export const metadata: Metadata = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Words that receive the looping read-along highlight sweep in the hero. This
|
||||||
|
// is the product's actual word-level highlighting, used as the page's signature.
|
||||||
|
const SWEEP = ['highlighted', 'word', 'by', 'word.'];
|
||||||
|
|
||||||
|
const PROVIDERS = ['Kokoro', 'KittenTTS', 'Orpheus', 'OpenAI', 'Replicate', 'DeepInfra'];
|
||||||
|
|
||||||
|
const FORMATS = ['EPUB', 'PDF', 'TXT', 'MD', 'DOCX'];
|
||||||
|
|
||||||
export default async function LandingPage() {
|
export default async function LandingPage() {
|
||||||
const runtimeConfig = await getResolvedRuntimeConfigForRsc();
|
const runtimeConfig = await getResolvedRuntimeConfigForRsc();
|
||||||
const enableUserSignups = runtimeConfig.enableUserSignups;
|
const enableUserSignups = runtimeConfig.enableUserSignups;
|
||||||
|
|
||||||
const instanceBadge =
|
const instanceBadge =
|
||||||
process.env.RICHARDRDEV_PRODUCTION === 'true'
|
process.env.RICHARDRDEV_PRODUCTION === 'true'
|
||||||
? 'Official OpenReader Instance'
|
? 'Official OpenReader instance'
|
||||||
: 'Open Source Document Reader';
|
: 'Open-source document reader';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="public-main">
|
<main className="public-main">
|
||||||
<div className="public-wrap">
|
{/* ───────────────────────────── Hero ───────────────────────────── */}
|
||||||
<section className="public-panel public-hero-shell public-reveal-1">
|
<section className="public-hero">
|
||||||
<article className="public-hero-panel">
|
<div className="public-wrap public-hero-grid">
|
||||||
<p className="public-eyebrow">{instanceBadge}</p>
|
<div className="public-hero-lede public-reveal-1">
|
||||||
<h1 className="public-hero-title">
|
<p className="public-eyebrow">
|
||||||
Turn static files into a <em>living read-along surface</em>.
|
<span className="public-eyebrow-dot" aria-hidden="true" />
|
||||||
</h1>
|
{instanceBadge}
|
||||||
<p className="public-hero-copy">
|
|
||||||
OpenReader is a self-host-friendly workspace for EPUB, PDF, TXT, MD, and DOCX. It combines
|
|
||||||
synchronized speech, fast navigation, and cloud or local TTS providers so long-form reading is easier
|
|
||||||
to sustain.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<h1 className="public-hero-title">
|
||||||
|
Hear every document,
|
||||||
|
<br />
|
||||||
|
<span className="public-sweep" aria-hidden="true">
|
||||||
|
{SWEEP.map((word, i) => (
|
||||||
|
<span
|
||||||
|
key={word + i}
|
||||||
|
className="public-sweep-word"
|
||||||
|
style={{ '--i': i } as CSSProperties}
|
||||||
|
>
|
||||||
|
{word}
|
||||||
|
{i < SWEEP.length - 1 ? ' ' : ''}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
<span className="public-visually-hidden">highlighted word by word.</span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p className="public-hero-copy">
|
||||||
|
OpenReader turns EPUB, PDF, TXT, Markdown, and DOCX into a
|
||||||
|
synchronized read-along surface, with genuine text-to-speech,
|
||||||
|
word-level highlighting, and audiobook export. It’s open
|
||||||
|
source, and entirely yours to self-host.
|
||||||
|
</p>
|
||||||
|
|
||||||
<div className="public-actions">
|
<div className="public-actions">
|
||||||
<Link href="/app" className={buttonClass({ variant: 'primary', size: 'lg' })}>
|
<Link href="/app" className={buttonClass({ variant: 'primary', size: 'lg' })}>
|
||||||
Open Reader Workspace
|
Open the reader
|
||||||
</Link>
|
|
||||||
<Link href="/signin" className={buttonClass({ variant: 'secondary', size: 'lg' })}>
|
|
||||||
Sign In
|
|
||||||
</Link>
|
</Link>
|
||||||
{enableUserSignups ? (
|
{enableUserSignups ? (
|
||||||
<Link href="/signup" className={buttonClass({ variant: 'outline', size: 'lg' })}>
|
<Link href="/signup" className={buttonClass({ variant: 'outline', size: 'lg' })}>
|
||||||
Create Account
|
Create account
|
||||||
</Link>
|
</Link>
|
||||||
) : null}
|
) : (
|
||||||
<Link href="https://docs.openreader.richardr.dev/" className={buttonClass({ variant: 'ghost', size: 'lg' })}>
|
<Link href="/signin" className={buttonClass({ variant: 'outline', size: 'lg' })}>
|
||||||
Read Docs
|
Sign in
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
<Link
|
||||||
|
href="https://docs.openreader.richardr.dev/"
|
||||||
|
className={buttonClass({ variant: 'ghost', size: 'lg' })}
|
||||||
|
>
|
||||||
|
Read the docs →
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<dl className="public-meta">
|
|
||||||
<div className="public-meta-item">
|
|
||||||
<dt className="public-meta-label">Input formats</dt>
|
|
||||||
<dd className="public-meta-value">EPUB, PDF, TXT, MD, DOCX</dd>
|
|
||||||
</div>
|
|
||||||
<div className="public-meta-item">
|
|
||||||
<dt className="public-meta-label">Read-along</dt>
|
|
||||||
<dd className="public-meta-value">Word-level sync highlighting</dd>
|
|
||||||
</div>
|
|
||||||
<div className="public-meta-item">
|
|
||||||
<dt className="public-meta-label">Providers</dt>
|
|
||||||
<dd className="public-meta-value">Replicate TTS and OpenAI-compatible</dd>
|
|
||||||
</div>
|
|
||||||
<div className="public-meta-item">
|
|
||||||
<dt className="public-meta-label">Export</dt>
|
|
||||||
<dd className="public-meta-value">Audiobook workflows</dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<aside className="public-signal-panel" aria-label="What makes OpenReader different">
|
<div className="public-formats" aria-label="Supported formats">
|
||||||
<p className="public-signal-title">Reader pipeline</p>
|
<span className="public-formats-label">Reads</span>
|
||||||
<ul className="public-signal-list">
|
<div className="public-formats-list">
|
||||||
<li className="public-signal-item">
|
{FORMATS.map((fmt) => (
|
||||||
<h3>Layout-aware parsing</h3>
|
<span key={fmt} className="public-format-chip">
|
||||||
<p>PDF pages are parsed into structured blocks for precise highlight alignment and consistent sentence boundaries.</p>
|
{fmt}
|
||||||
</li>
|
</span>
|
||||||
<li className="public-signal-item">
|
))}
|
||||||
<h3>Segment preloading</h3>
|
</div>
|
||||||
<p>Sentence-level audio segments are generated and cached, reducing stalls while you read continuously.</p>
|
</div>
|
||||||
</li>
|
</div>
|
||||||
<li className="public-signal-item">
|
|
||||||
<h3>Device sync</h3>
|
{/* Reader mockup: demonstrates the synchronized highlight + playback */}
|
||||||
<p>Progress, queue state, and document settings follow your account across browser sessions and devices.</p>
|
<aside className="public-reader public-reveal-2" aria-label="OpenReader playback preview">
|
||||||
</li>
|
<div className="public-reader-glow" aria-hidden="true" />
|
||||||
<li className="public-signal-item">
|
<div className="public-reader-bar">
|
||||||
<h3>Self-host path</h3>
|
<span className="public-reader-dot" data-tone="a" />
|
||||||
<p>Run with Docker, pair with S3 or SeaweedFS, and plug in hosted or self-managed TTS endpoints.</p>
|
<span className="public-reader-dot" data-tone="b" />
|
||||||
</li>
|
<span className="public-reader-dot" data-tone="c" />
|
||||||
</ul>
|
<span className="public-reader-file">wizard-of-oz.epub</span>
|
||||||
|
<span className="public-reader-voice">Kokoro · af_sky</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="public-reader-body">
|
||||||
|
<p className="public-reader-chapter">Chapter 1 · The Cyclone</p>
|
||||||
|
<p className="public-reader-text">
|
||||||
|
Dorothy lived in the midst of the great Kansas prairies, with
|
||||||
|
Uncle Henry, who was a farmer, and Aunt Em, who was the
|
||||||
|
farmer’s wife.{' '}
|
||||||
|
<span className="public-reader-sentence">
|
||||||
|
Their house was small, for the lumber to build it had to be
|
||||||
|
carried by <span className="public-reader-word">wagon</span>{' '}
|
||||||
|
many miles.
|
||||||
|
</span>{' '}
|
||||||
|
There were four walls, a floor and a roof, which made one room.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="public-player">
|
||||||
|
<button type="button" className="public-player-play" aria-label="Playing" tabIndex={-1}>
|
||||||
|
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
|
||||||
|
<path d="M8 5v14l11-7z" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div className="public-wave" aria-hidden="true">
|
||||||
|
{Array.from({ length: 28 }).map((_, i) => (
|
||||||
|
<span
|
||||||
|
key={i}
|
||||||
|
className="public-wave-bar"
|
||||||
|
style={{ '--b': i } as CSSProperties}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<span className="public-player-time">04:12 / 18:30</span>
|
||||||
|
<span className="public-player-speed">1.1×</span>
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</section>
|
</div>
|
||||||
|
|
||||||
<section className="public-section public-reveal-2" aria-labelledby="capability-heading">
|
<div className="public-wrap">
|
||||||
<div className="public-section-head">
|
<div className="public-providers public-reveal-3" aria-label="Supported text-to-speech providers">
|
||||||
<h2 id="capability-heading">Engine room capabilities</h2>
|
<span className="public-providers-label">Speaks through</span>
|
||||||
<p>Built for deep reading, not just playback demos.</p>
|
<div className="public-providers-track">
|
||||||
|
{PROVIDERS.map((p) => (
|
||||||
|
<span key={p} className="public-provider">
|
||||||
|
{p}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="public-feature-grid public-rail">
|
</div>
|
||||||
<article className="public-feature-card">
|
</section>
|
||||||
<span className="public-feature-kicker">Parsing</span>
|
|
||||||
<h3>Structured document understanding</h3>
|
|
||||||
<p>Geometry-aware parsing gives stable reading order and cleaner highlighting, especially for complex PDF layouts.</p>
|
|
||||||
</article>
|
|
||||||
<article className="public-feature-card">
|
|
||||||
<span className="public-feature-kicker">Alignment</span>
|
|
||||||
<h3>ONNX-powered timing maps</h3>
|
|
||||||
<p>Speech alignment data links each spoken unit back to text so the cursor follows naturally during playback.</p>
|
|
||||||
</article>
|
|
||||||
<article className="public-feature-card">
|
|
||||||
<span className="public-feature-kicker">Providers</span>
|
|
||||||
<h3>Cloud + self-hosted voices</h3>
|
|
||||||
<p>Use OpenAI, DeepInfra, Replicate, or OpenAI-compatible local servers such as Kokoro or KittenTTS.</p>
|
|
||||||
</article>
|
|
||||||
<article className="public-feature-card">
|
|
||||||
<span className="public-feature-kicker">Distribution</span>
|
|
||||||
<h3>Audiobook-ready output</h3>
|
|
||||||
<p>Export chapterized audio and keep metadata clean for downstream players, with resumable processing for large docs.</p>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="public-section public-reveal-3" aria-labelledby="workflow-heading">
|
{/* ────────────────────────── How it works ───────────────────────── */}
|
||||||
|
<section className="public-section" aria-labelledby="how-heading">
|
||||||
|
<div className="public-wrap">
|
||||||
<div className="public-section-head">
|
<div className="public-section-head">
|
||||||
<h2 id="workflow-heading">Typical workflow</h2>
|
<p className="public-kicker">The flow</p>
|
||||||
<p>Minimal friction from upload to listen.</p>
|
<h2 id="how-heading">From a raw file to a voice in three moves.</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="public-path public-rail">
|
|
||||||
<article className="public-path-step" data-step="01">
|
<ol className="public-steps">
|
||||||
<h3>Drop a file</h3>
|
<li className="public-step">
|
||||||
<p>Import a document into your library and keep it organized for repeat sessions.</p>
|
<span className="public-step-num">01</span>
|
||||||
</article>
|
<h3>Upload a document</h3>
|
||||||
<article className="public-path-step" data-step="02">
|
<p>
|
||||||
<h3>Choose a voice stack</h3>
|
Drop an EPUB, PDF, TXT, Markdown, or DOCX into your library, or
|
||||||
<p>Pick provider, model, and speed profile tuned to your reading pace.</p>
|
import one straight from the server, and it stays organized for
|
||||||
</article>
|
every session after.
|
||||||
<article className="public-path-step" data-step="03">
|
</p>
|
||||||
|
</li>
|
||||||
|
<li className="public-step">
|
||||||
|
<span className="public-step-num">02</span>
|
||||||
|
<h3>Pick a voice</h3>
|
||||||
|
<p>
|
||||||
|
Choose a provider and model: hosted OpenAI, Replicate, or
|
||||||
|
DeepInfra, or your own self-hosted Kokoro, KittenTTS, or Orpheus
|
||||||
|
server. Set the speed to your pace.
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
<li className="public-step">
|
||||||
|
<span className="public-step-num">03</span>
|
||||||
<h3>Read, listen, export</h3>
|
<h3>Read, listen, export</h3>
|
||||||
<p>Track progress with synchronized highlighting and export audiobook files when you need offline playback.</p>
|
<p>
|
||||||
|
Follow word-level highlighting as it plays, pick up where you
|
||||||
|
left off on any device, and export a chaptered m4b or mp3
|
||||||
|
audiobook for the road.
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ──────────────────────────── Features ─────────────────────────── */}
|
||||||
|
<section className="public-section" aria-labelledby="features-heading">
|
||||||
|
<div className="public-wrap">
|
||||||
|
<div className="public-section-head">
|
||||||
|
<p className="public-kicker">Under the hood</p>
|
||||||
|
<h2 id="features-heading">Engineered for deep reading, not playback demos.</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="public-features">
|
||||||
|
<article className="public-feature public-feature-wide">
|
||||||
|
<span className="public-feature-kicker">Parsing</span>
|
||||||
|
<h3>Layout-aware PDF understanding</h3>
|
||||||
|
<p>
|
||||||
|
PP-DocLayoutV3 (ONNX) detects structured blocks, stitches
|
||||||
|
content across pages, and aligns highlights to real geometry,
|
||||||
|
so even dense, multi-column PDFs read in the right order.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article className="public-feature">
|
||||||
|
<span className="public-feature-kicker">Alignment</span>
|
||||||
|
<h3>Word-by-word timing</h3>
|
||||||
|
<p>
|
||||||
|
ONNX Whisper alignment through a JetStream-backed compute worker
|
||||||
|
maps each spoken word back to the page, so the cursor tracks
|
||||||
|
speech precisely.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article className="public-feature">
|
||||||
|
<span className="public-feature-kicker">Voices</span>
|
||||||
|
<h3>Multi-provider TTS</h3>
|
||||||
|
<p>
|
||||||
|
Mix cloud APIs with OpenAI-compatible local servers. Bring your
|
||||||
|
own keys and endpoints, with no lock-in to a single vendor.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article className="public-feature">
|
||||||
|
<span className="public-feature-kicker">Export</span>
|
||||||
|
<h3>Audiobook output</h3>
|
||||||
|
<p>
|
||||||
|
Render chaptered m4b and mp3 files with resumable processing,
|
||||||
|
ready for any offline player you already use.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article className="public-feature">
|
||||||
|
<span className="public-feature-kicker">Sync</span>
|
||||||
|
<h3>Progress that follows you</h3>
|
||||||
|
<p>
|
||||||
|
Reading position, queue state, and per-document settings sync
|
||||||
|
across browser sessions and devices through your account.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article className="public-feature public-feature-wide">
|
||||||
|
<span className="public-feature-kicker">Backend</span>
|
||||||
|
<h3>A stack you actually control</h3>
|
||||||
|
<p>
|
||||||
|
Run on embedded SeaweedFS or any S3-compatible bucket, back it
|
||||||
|
with SQLite or Postgres, and ship it with Docker on amd64 or
|
||||||
|
arm64, with built-in auth and automatic startup migrations.
|
||||||
|
</p>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section className="public-panel public-callout public-reveal-3" aria-label="Open source call to action">
|
{/* ───────────────────────── Self-host CTA ───────────────────────── */}
|
||||||
<h2>Ship your own private reading stack.</h2>
|
<section className="public-section" aria-labelledby="selfhost-heading">
|
||||||
<p>
|
<div className="public-wrap">
|
||||||
Run OpenReader locally or deploy it for your team. The docs cover Docker setup, provider integration,
|
<div className="public-callout">
|
||||||
object storage, and compute worker configuration.
|
<div className="public-callout-glow" aria-hidden="true" />
|
||||||
</p>
|
<div className="public-callout-copy">
|
||||||
<div className="public-actions">
|
<p className="public-kicker">Open source · MIT</p>
|
||||||
<a
|
<h2 id="selfhost-heading">Run your own private reading stack.</h2>
|
||||||
href="https://github.com/richardr1126/openreader#readme"
|
<p className="public-callout-text">
|
||||||
target="_blank"
|
Deploy OpenReader for yourself or your team in minutes. The docs
|
||||||
rel="noopener noreferrer"
|
cover Docker, provider integration, object storage, and the
|
||||||
className={buttonClass({ variant: 'primary', size: 'md' })}
|
external compute worker. Every piece is yours to host.
|
||||||
>
|
</p>
|
||||||
View Repository
|
<div className="public-actions">
|
||||||
</a>
|
<a
|
||||||
<a
|
href="https://github.com/richardr1126/openreader#readme"
|
||||||
href="https://docs.openreader.richardr.dev/"
|
target="_blank"
|
||||||
target="_blank"
|
rel="noopener noreferrer"
|
||||||
rel="noopener noreferrer"
|
className={buttonClass({ variant: 'primary', size: 'lg' })}
|
||||||
className={buttonClass({ variant: 'outline', size: 'md' })}
|
>
|
||||||
>
|
View the repository
|
||||||
Deployment Guides
|
</a>
|
||||||
</a>
|
<a
|
||||||
|
href="https://docs.openreader.richardr.dev/docker-quick-start"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className={buttonClass({ variant: 'outline', size: 'lg' })}
|
||||||
|
>
|
||||||
|
Deployment guides
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="public-terminal" aria-hidden="true">
|
||||||
|
<div className="public-terminal-bar">
|
||||||
|
<span className="public-reader-dot" data-tone="a" />
|
||||||
|
<span className="public-reader-dot" data-tone="b" />
|
||||||
|
<span className="public-reader-dot" data-tone="c" />
|
||||||
|
<span className="public-terminal-title">quick start</span>
|
||||||
|
</div>
|
||||||
|
<pre className="public-terminal-body">
|
||||||
|
<code>
|
||||||
|
<span className="public-term-comment"># pull and run</span>
|
||||||
|
{'\n'}
|
||||||
|
<span className="public-term-prompt">$</span> docker run --name openreader \{'\n'}
|
||||||
|
{' '}-p <span className="public-term-accent">3003:3003</span> -p{' '}
|
||||||
|
<span className="public-term-accent">8333:8333</span> \{'\n'}
|
||||||
|
{' '}-e BASE_URL=
|
||||||
|
<span className="public-term-accent">http://localhost:3003</span> \{'\n'}
|
||||||
|
{' '}-e AUTH_SECRET=
|
||||||
|
<span className="public-term-accent">$(openssl rand -hex 32)</span> \{'\n'}
|
||||||
|
{' '}ghcr.io/richardr1126/openreader:latest{'\n'}
|
||||||
|
{'\n'}
|
||||||
|
<span className="public-term-comment"># open the reading room</span>
|
||||||
|
{'\n'}
|
||||||
|
<span className="public-term-prompt">$</span> open{' '}
|
||||||
|
<span className="public-term-accent">localhost:3003</span>
|
||||||
|
<span className="public-term-caret" />
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -197,7 +197,7 @@ export default async function PrivacyPage() {
|
||||||
Self-Hosting Guide
|
Self-Hosting Guide
|
||||||
</a>
|
</a>
|
||||||
<Link href="/?redirect=false" className={buttonClass({ variant: 'ghost', size: 'md' })}>
|
<Link href="/?redirect=false" className={buttonClass({ variant: 'ghost', size: 'md' })}>
|
||||||
Back to home
|
Back to landing
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue