A modern Next.js application for reading books in markdown format with full mathematical notation support using KaTeX.
- Multiple Book Support: Configure multiple book repositories
- Markdown Rendering: Full GitHub Flavored Markdown support
- Math Rendering: LaTeX math expressions with KaTeX
- Interactive TOC: Auto-generated table of contents with scroll sync
- Responsive Design: Works on desktop and mobile devices
- Japanese Text Support: Proper typography and full-width space indentation
- Static Generation: Fast page loads with Next.js SSG
- Dark Mode: Automatic dark mode support
- Full-Text Search: Algolia-powered search with Japanese language support
- Node.js 18+
- pnpm (install with
npm install -g pnpm)
- Clone this repository:
git clone <repository-url>
cd book-viewer- Install dependencies:
pnpm install-
Configure your books (see Configuration below)
-
Run the development server:
pnpm dev- Open http://localhost:3000 in your browser
The build process automatically copies images from book repositories to the output directory.
pnpm buildThe build process:
- Generates static HTML for all chapters
- Copies images from each book's
images/directory toout/{bookId}/images/ - Maintains relative paths so
../images/references work correctly
- Copy the example configuration file:
cp config/books.config.ts.example config/books.config.ts- Edit
config/books.config.tsto add paths to your book repositories:
export const BOOKS_CONFIG: string[] = [
'./books/mybook', // Relative path (recommended for books in project)
'~/Documents/another-book', // Home directory path
'/absolute/path/to/book', // Absolute path
]Supported path formats:
- Relative paths:
./books/mybook- relative to project root (recommended for books stored in the project) - Home directory:
~/Books/mybook- uses your home directory (expanded automatically) - Absolute paths:
/var/books/mybook- full system path
Note: config/books.config.ts is in .gitignore to keep your local book paths private.
Each book repository must contain a book.json file in its root directory:
{
"id": "unique-book-id",
"title": "Book Title",
"subtitle": "Optional Subtitle",
"author": "Author Name",
"publisher": "Publisher Name",
"year": "2024",
"description": "Brief description of the book",
"language": "en",
"chapters": [
{
"id": "chapter-1",
"title": "Chapter 1: Introduction",
"file": "chapters/01-introduction.md",
"order": 1
}
]
}id(string): Unique identifier for the booktitle(string): Book titlechapters(array): List of chapter objects
subtitle(string): Book subtitleauthor(string): Author name(s)publisher(string): Publisher nameyear(string): Publication yeardescription(string): Book descriptionlanguage(string): Language code (e.g., "en", "ja")cover(string): Path to cover image
id(string): Unique chapter identifiertitle(string): Chapter titlefile(string): Relative path to markdown file from book rootorder(number): Display orderdescription(string, optional): Chapter descriptionhidden(boolean, optional): Hide from navigation
Each book repository should follow this structure:
book-repository/
├── book.json # Book metadata (required)
├── chapters/ # Markdown files (required)
│ ├── 01-intro.md
│ ├── 02-chapter2.md
│ └── ...
└── images/ # Images referenced in markdown (optional)
├── fig-1.png
└── ...
Images: Store images in an images/ directory at the book root. Reference them in markdown with relative paths:
During build, images are automatically copied to the output directory maintaining the relative path structure, so ../images/ references work correctly from chapter pages.
md-book-viewer/
├── app/ # Next.js app directory
│ ├── [bookId]/
│ │ └── [chapterId]/
│ │ └── page.tsx # Chapter viewer page
│ ├── layout.tsx # Root layout
│ ├── page.tsx # Home page (book list)
│ └── globals.css # Global styles
├── components/ # React components
│ ├── BookList.tsx # Book listing component
│ ├── ChapterContent.tsx # Markdown content renderer
│ ├── Navigation.tsx # Chapter navigation
│ ├── Sidebar.tsx # Sidebar with TOC
│ └── TableOfContents.tsx # Interactive TOC
├── lib/ # Utility libraries
│ ├── books.ts # Book/chapter loading logic
│ ├── markdown.ts # Markdown processing
│ ├── toc.ts # TOC extraction
│ └── remark-japanese-indent.ts # Japanese text plugin
├── types/ # TypeScript types
│ └── index.ts # Type definitions
├── config/ # Configuration
│ └── books.config.ts # Book paths configuration
├── scripts/ # Build and utility scripts
│ ├── copy-book-images.js # Copies images from books to output
│ └── generate-book-json.js # Generates book.json template
└── out/ # Static build output (after build)
└── [bookId]/
├── images/ # Copied from book repo
└── [chapterId]/ # Chapter pages
Inline math: $E = mc^2$
Display math:
$$
\int_{-\infty}^{\infty} e^{-x^2} dx = \sqrt{\pi}
$$
- Tables
- Task lists
- Strikethrough
- Autolinks
Raw HTML elements are supported in markdown files.
Japanese text is properly formatted with:
- Appropriate font families
- Letter spacing for readability
- Full-width space ( ) indentation support
pnpm testpnpm lintnpx tsc --noEmitThis application is configured for static export - all pages are pre-rendered as HTML files at build time. The out/ directory contains fully static files that can be deployed anywhere:
pnpm build
# Deploy out/ directory to your hostingAfter building, you can test the static export locally using the same bun+hono server that runs in production:
# Serve on default port (3000)
pnpm serve
# Serve on a custom port
PORT=3010 pnpm serveThis runs server.ts with Bun, providing the same caching behavior and routing as the Docker deployment.
The application includes Docker support with a multi-stage build that uses bun + hono for efficient static file serving.
-
Ensure books are available: Docker
COPYdoesn't follow symlinks, so you must have actual book files in thebooks/directory (not just symlinks). -
Start with Docker Compose (recommended):
docker compose up -dThis will build the image and start the container in detached mode.
- View logs:
docker compose logs -f- Stop the container:
docker compose down- Access the application: Open http://localhost:3000
The Dockerfile implements a two-stage build:
-
Build stage (Dockerfile:8-30):
- Installs dependencies with pnpm
- Validates that
books/directory exists with actual content - Runs
pnpm buildto generate static files inout/
-
Production stage (Dockerfile:33-49):
- Uses lightweight Bun runtime
- Copies only the
out/directory and server files - Runs server.ts with Bun for optimized static serving
The docker-compose.example.yml includes Traefik labels for reverse proxy with HTTPS. See mu373/traefik for a basic Traefik setup with Let's Encrypt and Cloudflare DNS. To use:
-
Copy and customize the Host rule in your
docker-compose.yml:traefik.http.routers.book.rule: Host(`book.example.com`)
-
Ensure the external
traefik-nwnetwork exists and Traefik is configured -
DNS setup: Add a CNAME record pointing to your server. For example, if your Tailscale server is at
myserver.example.com, add:book.example.com CNAME myserver.example.com
After setup, the service will be available at https://book.example.com.
The application supports full-text search powered by Algolia.
-
Create an Algolia account (free tier: 10k records, 10k searches/month)
-
Create a new application and index
-
Copy
.env.exampleto.envand fill in your credentials:
cp .env.example .env# Server-side only (for indexing)
ALGOLIA_APP_ID=your_app_id
ALGOLIA_ADMIN_KEY=your_admin_key
ALGOLIA_INDEX_NAME=md-book-viewer
# Client-side (for search UI)
NEXT_PUBLIC_ALGOLIA_APP_ID=your_app_id
NEXT_PUBLIC_ALGOLIA_SEARCH_KEY=your_search_only_key
NEXT_PUBLIC_ALGOLIA_INDEX_NAME=md-book-viewer- Run the indexing script to push content to Algolia:
pnpm indexThis will:
- Read all books from
BOOKS_CONFIG - Extract text content from markdown files
- Split content into searchable chunks by headings
- Configure the index for Japanese language (
indexLanguages: ['ja']) - Upload records to Algolia
Run pnpm index whenever you:
- Add new books or chapters
- Update existing content
- Change the books configuration
- Press
⌘K(Mac) orCtrl+K(Windows/Linux) to open search - Toggle between searching current book or all books
- Results show chapter and section with highlighted matches
- Keyboard navigation with arrow keys and Enter
You can define synonyms and abbreviations in config/synonyms.json to improve search results. The indexing script automatically uploads these to Algolia.
{
"synonyms": [
{
"type": "synonym",
"synonyms": ["MCMC", "マルコフ連鎖モンテカルロ法", "Markov chain Monte Carlo"]
},
{
"type": "synonym",
"synonyms": ["GNN", "グラフニューラルネットワーク", "Graph Neural Network"]
}
]
}Searching any term in a synonym group will find results containing any of the other terms.
- Next.js 16 - React framework with App Router and static export
- TypeScript - Type safety
- Tailwind CSS 4 - Styling
- KaTeX - Math rendering
- Remark/Rehype - Markdown processing
- Unified - Content transformation
- Bun - Fast JavaScript runtime for production serving (Docker)
- Hono - Lightweight web framework for static file serving (Docker)
MIT License.