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>
<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>
<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://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>
</section>
</div>
<section className="public-section public-reveal-2" aria-labelledby="capability-heading">
<div className="public-section-head">
<h2 id="capability-heading">Engine room capabilities</h2>
<p>Built for deep reading, not just playback demos.</p>
<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 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>
</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">
<h2 id="workflow-heading">Typical workflow</h2>
<p>Minimal friction from upload to listen.</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-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">
<ol className="public-steps">
<li className="public-step">
<span className="public-step-num">01</span>
<h3>Upload a document</h3>
<p>
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>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>
</div>
</section>
</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>
<p>
Run OpenReader locally or deploy it for your team. The docs cover Docker setup, provider integration,
object storage, and compute worker configuration.
</p>
<div className="public-actions">
<a
href="https://github.com/richardr1126/openreader#readme"
target="_blank"
rel="noopener noreferrer"
className={buttonClass({ variant: 'primary', size: 'md' })}
>
View Repository
</a>
<a
href="https://docs.openreader.richardr.dev/"
target="_blank"
rel="noopener noreferrer"
className={buttonClass({ variant: 'outline', size: 'md' })}
>
Deployment Guides
</a>
{/* ───────────────────────── 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: 'lg' })}
>
View the repository
</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>
</section>
</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