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:
Richard R 2026-05-31 15:55:41 -06:00
parent 14807a0e8c
commit 535d562a0b
4 changed files with 1259 additions and 626 deletions

View file

@ -10,7 +10,9 @@ export default async function PublicLayout({ children }: { children: ReactNode }
return (
<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-topbar">
<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">
{/* eslint-disable-next-line @next/next/no-img-element */}
<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-tag">Read + Listen</span>
<span className="public-brand-tag">Read&nbsp;·&nbsp;Listen</span>
</span>
</Link>
<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
href="https://docs.openreader.richardr.dev/"
className={buttonClass({ variant: 'ghost', size: 'sm' })}
className="public-nav-link"
>
Docs
</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>
</header>
</div>
@ -52,19 +58,67 @@ export default async function PublicLayout({ children }: { children: ReactNode }
<footer className="public-footer">
<div className="public-wrap">
<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-links">
<Link href="/privacy">Privacy</Link>
<div className="public-footer-brand">
<div className="public-footer-mark">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src="/icon.svg" alt="" className="public-brand-mark" aria-hidden="true" />
<span className="public-brand-text">OpenReader</span>
</div>
<p className="public-footer-label">
An open-source reading room that turns documents into
synchronized, listenable audio that&rsquo;s yours to self-host.
</p>
<div className="public-footer-cta">
{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>
<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://docs.openreader.richardr.dev/" target="_blank" rel="noopener noreferrer">
Documentation
<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 &amp; 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>
</footer>

View file

@ -1,3 +1,4 @@
import type { CSSProperties } from 'react';
import type { Metadata } from 'next';
import Link from 'next/link';
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() {
const runtimeConfig = await getResolvedRuntimeConfigForRsc();
const enableUserSignups = runtimeConfig.enableUserSignups;
const instanceBadge =
process.env.RICHARDRDEV_PRODUCTION === 'true'
? 'Official OpenReader Instance'
: 'Open Source Document Reader';
? 'Official OpenReader instance'
: 'Open-source document reader';
return (
<main className="public-main">
<div className="public-wrap">
<section className="public-panel public-hero-shell public-reveal-1">
<article className="public-hero-panel">
<p className="public-eyebrow">{instanceBadge}</p>
<h1 className="public-hero-title">
Turn static files into a <em>living read-along surface</em>.
</h1>
<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.
{/* ───────────────────────────── Hero ───────────────────────────── */}
<section className="public-hero">
<div className="public-wrap public-hero-grid">
<div className="public-hero-lede public-reveal-1">
<p className="public-eyebrow">
<span className="public-eyebrow-dot" aria-hidden="true" />
{instanceBadge}
</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&rsquo;s open
source, and entirely yours to self-host.
</p>
<div className="public-actions">
<Link href="/app" className={buttonClass({ variant: 'primary', size: 'lg' })}>
Open Reader Workspace
</Link>
<Link href="/signin" className={buttonClass({ variant: 'secondary', size: 'lg' })}>
Sign In
Open the reader
</Link>
{enableUserSignups ? (
<Link href="/signup" className={buttonClass({ variant: 'outline', size: 'lg' })}>
Create Account
Create account
</Link>
) : null}
<Link href="https://docs.openreader.richardr.dev/" className={buttonClass({ variant: 'ghost', size: 'lg' })}>
Read Docs
) : (
<Link href="/signin" className={buttonClass({ variant: 'outline', size: 'lg' })}>
Sign in
</Link>
)}
<Link
href="https://docs.openreader.richardr.dev/"
className={buttonClass({ variant: 'ghost', size: 'lg' })}
>
Read the docs
</Link>
</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">
<p className="public-signal-title">Reader pipeline</p>
<ul className="public-signal-list">
<li className="public-signal-item">
<h3>Layout-aware parsing</h3>
<p>PDF pages are parsed into structured blocks for precise highlight alignment and consistent sentence boundaries.</p>
</li>
<li className="public-signal-item">
<h3>Segment preloading</h3>
<p>Sentence-level audio segments are generated and cached, reducing stalls while you read continuously.</p>
</li>
<li className="public-signal-item">
<h3>Device sync</h3>
<p>Progress, queue state, and document settings follow your account across browser sessions and devices.</p>
</li>
<li className="public-signal-item">
<h3>Self-host path</h3>
<p>Run with Docker, pair with S3 or SeaweedFS, and plug in hosted or self-managed TTS endpoints.</p>
</li>
</ul>
<div className="public-formats" aria-label="Supported formats">
<span className="public-formats-label">Reads</span>
<div className="public-formats-list">
{FORMATS.map((fmt) => (
<span key={fmt} className="public-format-chip">
{fmt}
</span>
))}
</div>
</div>
</div>
{/* Reader mockup: demonstrates the synchronized highlight + playback */}
<aside className="public-reader public-reveal-2" aria-label="OpenReader playback preview">
<div className="public-reader-glow" aria-hidden="true" />
<div className="public-reader-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-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&rsquo;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>
</div>
<div className="public-wrap">
<div className="public-providers public-reveal-3" aria-label="Supported text-to-speech providers">
<span className="public-providers-label">Speaks through</span>
<div className="public-providers-track">
{PROVIDERS.map((p) => (
<span key={p} className="public-provider">
{p}
</span>
))}
</div>
</div>
</div>
</section>
<section className="public-section public-reveal-2" aria-labelledby="capability-heading">
{/* ────────────────────────── How it works ───────────────────────── */}
<section className="public-section" aria-labelledby="how-heading">
<div className="public-wrap">
<div className="public-section-head">
<h2 id="capability-heading">Engine room capabilities</h2>
<p>Built for deep reading, not just playback demos.</p>
<p className="public-kicker">The flow</p>
<h2 id="how-heading">From a raw file to a voice in three moves.</h2>
</div>
<div className="public-feature-grid public-rail">
<article className="public-feature-card">
<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">
<div className="public-section-head">
<h2 id="workflow-heading">Typical workflow</h2>
<p>Minimal friction from upload to listen.</p>
</div>
<div className="public-path public-rail">
<article className="public-path-step" data-step="01">
<h3>Drop a file</h3>
<p>Import a document into your library and keep it organized for repeat sessions.</p>
</article>
<article className="public-path-step" data-step="02">
<h3>Choose a voice stack</h3>
<p>Pick provider, model, and speed profile tuned to your reading pace.</p>
</article>
<article className="public-path-step" data-step="03">
<h3>Read, listen, export</h3>
<p>Track progress with synchronized highlighting and export audiobook files when you need offline playback.</p>
</article>
</div>
</section>
<section className="public-panel public-callout public-reveal-3" aria-label="Open source call to action">
<h2>Ship your own private reading stack.</h2>
<ol className="public-steps">
<li className="public-step">
<span className="public-step-num">01</span>
<h3>Upload a document</h3>
<p>
Run OpenReader locally or deploy it for your team. The docs cover Docker setup, provider integration,
object storage, and compute worker configuration.
Drop an EPUB, PDF, TXT, Markdown, or DOCX into your library, or
import one straight from the server, and it stays organized for
every session after.
</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>
<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>
</div>
</div>
</section>
{/* ───────────────────────── Self-host CTA ───────────────────────── */}
<section className="public-section" aria-labelledby="selfhost-heading">
<div className="public-wrap">
<div className="public-callout">
<div className="public-callout-glow" aria-hidden="true" />
<div className="public-callout-copy">
<p className="public-kicker">Open source · MIT</p>
<h2 id="selfhost-heading">Run your own private reading stack.</h2>
<p className="public-callout-text">
Deploy OpenReader for yourself or your team in minutes. The docs
cover Docker, provider integration, object storage, and the
external compute worker. Every piece is yours to host.
</p>
<div className="public-actions">
<a
href="https://github.com/richardr1126/openreader#readme"
target="_blank"
rel="noopener noreferrer"
className={buttonClass({ variant: 'primary', size: 'md' })}
className={buttonClass({ variant: 'primary', size: 'lg' })}
>
View Repository
View the repository
</a>
<a
href="https://docs.openreader.richardr.dev/"
href="https://docs.openreader.richardr.dev/docker-quick-start"
target="_blank"
rel="noopener noreferrer"
className={buttonClass({ variant: 'outline', size: 'md' })}
className={buttonClass({ variant: 'outline', size: 'lg' })}
>
Deployment Guides
Deployment guides
</a>
</div>
</section>
</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>
</main>
);
}

View file

@ -197,7 +197,7 @@ export default async function PrivacyPage() {
Self-Hosting Guide
</a>
<Link href="/?redirect=false" className={buttonClass({ variant: 'ghost', size: 'md' })}>
Back to home
Back to landing
</Link>
</div>
</section>

File diff suppressed because it is too large Load diff