diff --git a/Dockerfile b/Dockerfile index 7a18dbad..d3bdf71c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,6 +2,9 @@ FROM node:18-alpine WORKDIR /app +# Install git for cloning the bible repository +RUN apk add --no-cache git + # Copy package files COPY package*.json ./ @@ -11,6 +14,12 @@ RUN npm ci --only=production # Copy application code COPY . . +# Clone ESV Bible markdown repository +RUN git clone https://github.com/lguenth/mdbible.git /tmp/mdbible && \ + mkdir -p /app/bible-data && \ + cp -r /tmp/mdbible/by_chapter/* /app/bible-data/ && \ + rm -rf /tmp/mdbible + # Expose port EXPOSE 3000 diff --git a/README.md b/README.md index 029c7292..5701d1ed 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,20 @@ # ESV Bible Markdown -A Docker-based web service for serving the ESV Bible in Markdown format. +A Docker-based web service for serving the ESV Bible in Markdown format with chapter-by-chapter organization. ## Features -- Complete ESV Bible text in Markdown format +- Complete ESV Bible text in Markdown format from [lguenth/mdbible](https://github.com/lguenth/mdbible) +- Organized by book and chapter for easy navigation - Docker containerized for easy deployment - RESTful API for accessing Bible content +- Persistent volume storage for Bible data - Optimized for remote hosting ## Setup 1. Clone this repository -2. Place ESV Bible Markdown files in the `bible-data` directory +2. The ESV Bible data will be automatically downloaded during Docker build from the GitHub repository 3. Run `docker-compose up --build` ## Usage @@ -21,10 +23,24 @@ The service will be available at `http://localhost:3000` ### API Endpoints -- `GET /books` - List all books -- `GET /books/:book` - Get specific book +- `GET /health` - Health check endpoint +- `GET /books` - List all available books +- `GET /books/:book` - Get complete book (all chapters combined) - `GET /books/:book/:chapter` - Get specific chapter +### Example Usage + +```bash +# List all books +curl http://localhost:3000/books + +# Get the book of Genesis +curl http://localhost:3000/books/Genesis + +# Get Genesis chapter 1 +curl http://localhost:3000/books/Genesis/1 +``` + ## Development For local development: @@ -40,6 +56,12 @@ Build and run with Docker Compose: docker-compose up --build ``` +The Bible data is stored in a persistent Docker volume named `bible_data` for efficient storage and updates. + +## Data Source + +Bible content is sourced from [lguenth/mdbible](https://github.com/lguenth/mdbible/tree/main/by_chapter), which provides the ESV Bible organized by book and chapter in Markdown format. + ## License MIT diff --git a/docker-compose.yml b/docker-compose.yml index bfaf6fd9..4d4ad6a4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,12 +1,14 @@ -version: '3.8' - services: esv-bible: build: . ports: - "3000:3000" volumes: - - ./bible-data:/app/bible-data:ro + - bible_data:/app/bible-data environment: - NODE_ENV=production restart: unless-stopped + +volumes: + bible_data: + driver: local diff --git a/src/index.js b/src/index.js index 7ec057a2..13678566 100644 --- a/src/index.js +++ b/src/index.js @@ -28,9 +28,28 @@ async function readMarkdownFile(filePath) { // Helper function to get all books async function getBooks() { try { - const files = await fs.readdir(BIBLE_DATA_DIR); - const markdownFiles = files.filter(file => file.endsWith('.md')); - return markdownFiles.map(file => file.replace('.md', '')); + const items = await fs.readdir(BIBLE_DATA_DIR); + const bookDirs = []; + + for (const item of items) { + const itemPath = path.join(BIBLE_DATA_DIR, item); + const stat = await fs.stat(itemPath); + + if (stat.isDirectory()) { + // Check if directory contains markdown files + try { + const files = await fs.readdir(itemPath); + if (files.some(file => file.endsWith('.md'))) { + bookDirs.push(item); + } + } catch (error) { + // Skip directories we can't read + continue; + } + } + } + + return bookDirs; } catch (error) { throw new Error('Failed to read bible data directory'); } @@ -53,10 +72,31 @@ app.get('/books', async (req, res) => { app.get('/books/:book', async (req, res) => { try { const { book } = req.params; - const filePath = path.join(BIBLE_DATA_DIR, `${book}.md`); + const bookDir = path.join(BIBLE_DATA_DIR, book); - const content = await readMarkdownFile(filePath); - res.type('text/markdown').send(content); + // Check if book directory exists + const stat = await fs.stat(bookDir); + if (!stat.isDirectory()) { + return res.status(404).json({ error: `Book '${book}' not found` }); + } + + // Get all chapter files + const files = await fs.readdir(bookDir); + const chapterFiles = files.filter(file => file.endsWith('.md')).sort(); + + if (chapterFiles.length === 0) { + return res.status(404).json({ error: `No chapters found for book '${book}'` }); + } + + // Combine all chapters + let fullBook = `# ${book}\n\n`; + for (const chapterFile of chapterFiles) { + const chapterPath = path.join(bookDir, chapterFile); + const chapterContent = await readMarkdownFile(chapterPath); + fullBook += chapterContent + '\n\n'; + } + + res.type('text/markdown').send(fullBook); } catch (error) { res.status(404).json({ error: `Book '${req.params.book}' not found` }); } @@ -65,35 +105,12 @@ app.get('/books/:book', async (req, res) => { app.get('/books/:book/:chapter', async (req, res) => { try { const { book, chapter } = req.params; - const filePath = path.join(BIBLE_DATA_DIR, `${book}.md`); + const chapterPath = path.join(BIBLE_DATA_DIR, book, `${chapter}.md`); - const content = await readMarkdownFile(filePath); - - // Simple chapter extraction (this could be improved with better parsing) - const lines = content.split('\n'); - const chapterPattern = new RegExp(`^# ${chapter}\\s*$`, 'i'); - const chapterLines = []; - let inChapter = false; - - for (const line of lines) { - if (chapterPattern.test(line)) { - inChapter = true; - chapterLines.push(line); - } else if (inChapter && line.startsWith('# ')) { - // Next chapter starts - break; - } else if (inChapter) { - chapterLines.push(line); - } - } - - if (chapterLines.length === 0) { - return res.status(404).json({ error: `Chapter ${chapter} not found in ${book}` }); - } - - res.type('text/markdown').send(chapterLines.join('\n')); + const content = await readMarkdownFile(chapterPath); + res.type('text/markdown').send(content); } catch (error) { - res.status(500).json({ error: error.message }); + res.status(404).json({ error: `Chapter ${chapter} not found in book '${book}'` }); } });