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 (
|
||||
<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 · 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’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 & 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>
|
||||
|
|
|
|||
|
|
@ -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’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’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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue