Skip to main content

nbviewer: Jupyter Notebook Sharing Platform

nbviewer is a web application for sharing static renderings of Jupyter notebooks. It provides a simple way to share notebooks publicly by rendering them as static HTML, making them accessible to anyone without requiring a Jupyter environment.

Overview

What is nbviewer?

nbviewer is a service that:

  • Renders Jupyter Notebooks: Converts .ipynb files to static HTML
  • Public Sharing: Makes notebooks accessible via URLs
  • No Installation Required: Viewers don't need Jupyter installed
  • Multiple Sources: Supports GitHub, Gist, URLs, and file uploads
  • Preserves Formatting: Maintains notebook formatting, plots, and outputs

Key Features

  • Static Rendering: Safe, read-only notebook viewing
  • Multiple Input Sources: GitHub repos, Gists, direct URLs, file uploads
  • Format Support: Supports all Jupyter notebook formats and outputs
  • Responsive Design: Works on desktop and mobile devices
  • Embeddable: Can be embedded in other websites
  • Open Source: Self-hostable and customizable

Architecture

┌─────────────────────────────────────────┐
│ User Request │
│ (GitHub URL, Gist, File Upload) │
└─────────────────┬───────────────────────┘

┌─────────────────▼───────────────────────┐
│ nbviewer Web App │
│ ┌─────────────┐ ┌─────────────────┐ │
│ │ Tornado │ │ URL Handler │ │
│ │ Server │ │ │ │
│ └─────────────┘ └─────────────────┘ │
└─────────────────┬───────────────────────┘

┌─────────────────▼───────────────────────┐
│ Notebook Processing │
│ ┌─────────────┐ ┌─────────────────┐ │
│ │ Fetcher │ │ Renderer │ │
│ │ │ │ (nbconvert) │ │
│ └─────────────┘ └─────────────────┘ │
└─────────────────┬───────────────────────┘

┌─────────────────▼───────────────────────┐
│ Static HTML Output │
│ (Cached & Served) │
└─────────────────────────────────────────┘

Installation and Setup

Prerequisites

# Python 3.6+
python --version

# Git (for GitHub integration)
git --version

# Node.js (for some frontend dependencies)
node --version

Installation Methods

1. pip Installation

# Install nbviewer
pip install nbviewer

# Install additional dependencies
pip install jupyter
pip install tornado
pip install requests

# Verify installation
nbviewer --help

2. Conda Installation

# Install from conda-forge
conda install -c conda-forge nbviewer

# Or create dedicated environment
conda create -n nbviewer python=3.9 nbviewer
conda activate nbviewer

3. Docker Installation

# Pull official nbviewer image
docker pull jupyter/nbviewer

# Run nbviewer container
docker run -d \
--name nbviewer \
-p 8080:8080 \
jupyter/nbviewer

# Access at http://localhost:8080

4. From Source

# Clone repository
git clone https://github.com/jupyter/nbviewer.git
cd nbviewer

# Install dependencies
pip install -r requirements.txt

# Install in development mode
pip install -e .

# Run development server
python -m nbviewer --debug

Basic Configuration

Configuration File (nbviewer_config.py)

# Basic nbviewer configuration

# Server settings
c.NBViewer.port = 8080
c.NBViewer.ip = '0.0.0.0'

# GitHub integration
c.NBViewer.github_oauth_key = 'your-github-oauth-key'
c.NBViewer.github_oauth_secret = 'your-github-oauth-secret'

# Caching
c.NBViewer.cache_expiry_min = 10 # Cache for 10 minutes
c.NBViewer.cache_expiry_max = 120 # Maximum cache time

# Rate limiting
c.NBViewer.rate_limit_interval = 60 # 1 minute
c.NBViewer.rate_limit_requests = 60 # 60 requests per minute

# Local file serving (if enabled)
c.NBViewer.local_handler = 'nbviewer.handlers.LocalFileHandler'
c.NBViewer.local_file_path = '/path/to/notebooks'

# Logging
c.NBViewer.log_level = 'INFO'

# Template customization
c.NBViewer.template_path = '/path/to/custom/templates'
c.NBViewer.static_path = '/path/to/custom/static'

Core Features and Usage

1. Viewing Notebooks from GitHub

URL Format

# GitHub repository notebook
https://nbviewer.org/github/{user}/{repo}/blob/{branch}/{path_to_notebook.ipynb}

# Examples:
https://nbviewer.org/github/jakevdp/PythonDataScienceHandbook/blob/master/notebooks/01.00-IPython-Beyond-Normal-Python.ipynb

https://nbviewer.org/github/pandas-dev/pandas/blob/main/doc/source/user_guide/10min.ipynb

GitHub Integration Features

# Automatic GitHub authentication for private repos
import os
from nbviewer.providers import GitHubProvider

# Configure GitHub provider
github_provider = GitHubProvider(
oauth_key=os.environ.get('GITHUB_OAUTH_KEY'),
oauth_secret=os.environ.get('GITHUB_OAUTH_SECRET')
)

# Access private repositories (with proper authentication)
private_notebook_url = "https://nbviewer.org/github/private-org/private-repo/blob/main/analysis.ipynb"

2. Viewing Notebooks from Gist

# GitHub Gist notebook
https://nbviewer.org/gist/{gist_id}

# Example:
https://nbviewer.org/gist/fperez/9716279

3. Viewing Notebooks from URLs

# Direct URL to notebook file
https://nbviewer.org/url/{notebook_url}

# Example:
https://nbviewer.org/url/example.com/path/to/notebook.ipynb

4. File Upload Interface

<!-- HTML form for file upload -->
<form action="/create" method="post" enctype="multipart/form-data">
<input type="file" name="file" accept=".ipynb" />
<input type="submit" value="Upload Notebook" />
</form>

Self-Hosting nbviewer

1. Basic Setup

# Create nbviewer directory
mkdir nbviewer-server
cd nbviewer-server

# Create configuration
cat > nbviewer_config.py << EOF
c.NBViewer.port = 8080
c.NBViewer.ip = '0.0.0.0'
c.NBViewer.cache_expiry_min = 10
c.NBViewer.cache_expiry_max = 120
EOF

# Run nbviewer
nbviewer --config=nbviewer_config.py

2. Production Deployment

Docker Compose Setup

# docker-compose.yml
version: "3.8"

services:
nbviewer:
image: jupyter/nbviewer:latest
ports:
- "8080:8080"
environment:
- GITHUB_OAUTH_KEY=${GITHUB_OAUTH_KEY}
- GITHUB_OAUTH_SECRET=${GITHUB_OAUTH_SECRET}
volumes:
- ./nbviewer_config.py:/srv/nbviewer/nbviewer_config.py
- ./cache:/srv/nbviewer/cache
restart: unless-stopped

nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./ssl:/etc/nginx/ssl
depends_on:
- nbviewer
restart: unless-stopped

redis:
image: redis:alpine
volumes:
- redis_data:/data
restart: unless-stopped

volumes:
redis_data:

Nginx Configuration

# nginx.conf
events {
worker_connections 1024;
}

http {
upstream nbviewer {
server nbviewer:8080;
}

# Rate limiting
limit_req_zone $binary_remote_addr zone=nbviewer_limit:10m rate=10r/s;

server {
listen 80;
server_name your-domain.com;

# Redirect to HTTPS
return 301 https://$server_name$request_uri;
}

server {
listen 443 ssl http2;
server_name your-domain.com;

# SSL configuration
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;

# Security headers
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";

# Caching for static assets
location /static/ {
expires 1y;
add_header Cache-Control "public, immutable";
}

# Rate limiting
location / {
limit_req zone=nbviewer_limit burst=20 nodelay;

proxy_pass http://nbviewer;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

# Increase timeout for large notebooks
proxy_read_timeout 300s;
proxy_connect_timeout 75s;
}
}
}

3. Advanced Configuration

Custom Providers

# custom_provider.py
from nbviewer.providers import BaseProvider
import requests

class CustomProvider(BaseProvider):
"""Custom provider for internal notebook storage"""

def __init__(self, **kwargs):
super().__init__(**kwargs)
self.base_url = kwargs.get('base_url', 'https://internal-storage.com')
self.api_key = kwargs.get('api_key')

async def get_notebook(self, path):
"""Fetch notebook from custom storage"""
url = f"{self.base_url}/api/notebooks/{path}"
headers = {'Authorization': f'Bearer {self.api_key}'}

response = requests.get(url, headers=headers)
if response.status_code == 200:
return response.json()
else:
raise ValueError(f"Notebook not found: {path}")

def get_notebook_url(self, path):
"""Generate URL for notebook"""
return f"/custom/{path}"

# Register custom provider
c.NBViewer.provider_rewrites = {
r'/custom/(.*)': CustomProvider(
base_url='https://internal-storage.com',
api_key='your-api-key'
)
}

Caching Configuration

# Advanced caching with Redis
import redis
from nbviewer.cache import RedisCache

# Redis cache configuration
c.NBViewer.cache_class = RedisCache
c.NBViewer.cache_config = {
'host': 'redis',
'port': 6379,
'db': 0,
'password': None,
'socket_timeout': 5,
'socket_connect_timeout': 5
}

# Cache settings
c.NBViewer.cache_expiry_min = 600 # 10 minutes
c.NBViewer.cache_expiry_max = 3600 # 1 hour
c.NBViewer.cache_size = 1000 # Maximum cached items

Customization and Theming

1. Custom Templates

<!-- custom_template.html -->
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}Custom nbviewer{% endblock %}</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />

<!-- Custom CSS -->
<link rel="stylesheet" href="{{ static_url('custom.css') }}" />

{% block extra_head %}{% endblock %}
</head>
<body>
<header class="navbar">
<div class="container">
<a class="navbar-brand" href="/">
<img src="{{ static_url('logo.png') }}" alt="Custom nbviewer" />
</a>

<nav class="navbar-nav">
<a href="/faq">FAQ</a>
<a href="/about">About</a>
</nav>
</div>
</header>

<main class="container">{% block main %}{% endblock %}</main>

<footer>
<div class="container">
<p>&copy; 2023 Your Organization</p>
</div>
</footer>

{% block extra_script %}{% endblock %}
</body>
</html>

2. Custom CSS

/* custom.css */
:root {
--primary-color: #2e86ab;
--secondary-color: #a23b72;
--background-color: #f8f9fa;
--text-color: #333;
}

.navbar {
background-color: var(--primary-color);
padding: 1rem 0;
}

.navbar-brand img {
height: 40px;
}

.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
}

/* Notebook styling */
.notebook {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin: 2rem 0;
padding: 2rem;
}

.cell {
margin-bottom: 1rem;
border-left: 3px solid var(--primary-color);
padding-left: 1rem;
}

.code-cell {
background-color: #f8f9fa;
border-radius: 4px;
padding: 1rem;
font-family: "Monaco", "Consolas", monospace;
}

.output {
margin-top: 0.5rem;
padding: 0.5rem;
background-color: white;
border: 1px solid #e9ecef;
border-radius: 4px;
}

/* Responsive design */
@media (max-width: 768px) {
.container {
padding: 0 0.5rem;
}

.notebook {
margin: 1rem 0;
padding: 1rem;
}
}

3. Custom JavaScript

// custom.js
document.addEventListener("DOMContentLoaded", function () {
// Add copy button to code cells
const codeCells = document.querySelectorAll(".code-cell");

codeCells.forEach(function (cell) {
const copyButton = document.createElement("button");
copyButton.textContent = "Copy";
copyButton.className = "copy-button";
copyButton.onclick = function () {
navigator.clipboard.writeText(cell.textContent);
copyButton.textContent = "Copied!";
setTimeout(() => {
copyButton.textContent = "Copy";
}, 2000);
};

cell.appendChild(copyButton);
});

// Add table of contents for long notebooks
generateTableOfContents();

// Add syntax highlighting
if (typeof hljs !== "undefined") {
hljs.highlightAll();
}
});

function generateTableOfContents() {
const headers = document.querySelectorAll("h1, h2, h3, h4, h5, h6");
if (headers.length < 3) return; // Only generate TOC for longer notebooks

const toc = document.createElement("div");
toc.className = "table-of-contents";
toc.innerHTML = "<h3>Table of Contents</h3>";

const tocList = document.createElement("ul");

headers.forEach(function (header, index) {
const id = `header-${index}`;
header.id = id;

const listItem = document.createElement("li");
listItem.className = `toc-${header.tagName.toLowerCase()}`;

const link = document.createElement("a");
link.href = `#${id}`;
link.textContent = header.textContent;

listItem.appendChild(link);
tocList.appendChild(listItem);
});

toc.appendChild(tocList);

// Insert TOC after the first cell
const firstCell = document.querySelector(".cell");
if (firstCell) {
firstCell.parentNode.insertBefore(toc, firstCell.nextSibling);
}
}

API and Integration

1. REST API

# API endpoints for programmatic access
import requests
import json

class NBViewerAPI:
def __init__(self, base_url='https://nbviewer.org'):
self.base_url = base_url

def render_github_notebook(self, user, repo, path, branch='main'):
"""Render notebook from GitHub repository"""
url = f"{self.base_url}/github/{user}/{repo}/blob/{branch}/{path}"
return self._make_request(url)

def render_gist_notebook(self, gist_id):
"""Render notebook from GitHub Gist"""
url = f"{self.base_url}/gist/{gist_id}"
return self._make_request(url)

def render_url_notebook(self, notebook_url):
"""Render notebook from direct URL"""
url = f"{self.base_url}/url/{notebook_url}"
return self._make_request(url)

def _make_request(self, url):
"""Make HTTP request and return response"""
response = requests.get(url)
return {
'status_code': response.status_code,
'content': response.text,
'url': response.url
}

# Usage example
api = NBViewerAPI()
result = api.render_github_notebook(
'jakevdp',
'PythonDataScienceHandbook',
'notebooks/01.00-IPython-Beyond-Normal-Python.ipynb'
)

2. Embedding Notebooks

<!-- Embed notebook in webpage -->
<iframe
src="https://nbviewer.org/github/user/repo/blob/main/notebook.ipynb"
width="100%"
height="600"
frameborder="0"
>
</iframe>

<!-- Responsive embedding -->
<div class="notebook-embed">
<iframe
src="https://nbviewer.org/github/user/repo/blob/main/notebook.ipynb"
frameborder="0"
>
</iframe>
</div>

<style>
.notebook-embed {
position: relative;
padding-bottom: 56.25%; /* 16:9 aspect ratio */
height: 0;
overflow: hidden;
}

.notebook-embed iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
</style>

3. Webhook Integration

# Webhook handler for automatic notebook updates
from tornado.web import RequestHandler
import json
import subprocess

class GitHubWebhookHandler(RequestHandler):
"""Handle GitHub webhooks for automatic notebook updates"""

def post(self):
payload = json.loads(self.request.body)

# Verify webhook signature (recommended for security)
if not self.verify_signature(payload):
self.set_status(403)
return

# Check if push contains notebook files
if self.contains_notebooks(payload):
self.invalidate_cache(payload)
self.set_status(200)
self.write({'status': 'success'})
else:
self.set_status(200)
self.write({'status': 'no_notebooks'})

def verify_signature(self, payload):
"""Verify GitHub webhook signature"""
# Implement signature verification
return True

def contains_notebooks(self, payload):
"""Check if push contains .ipynb files"""
for commit in payload.get('commits', []):
for file_path in commit.get('added', []) + commit.get('modified', []):
if file_path.endswith('.ipynb'):
return True
return False

def invalidate_cache(self, payload):
"""Invalidate cache for updated notebooks"""
repo_full_name = payload['repository']['full_name']

# Clear cache for this repository
cache_key = f"github:{repo_full_name}:*"
# Implementation depends on your cache backend
self.application.cache.delete_pattern(cache_key)

# Add webhook handler to nbviewer
c.NBViewer.extra_handlers = [
(r'/webhook/github', GitHubWebhookHandler)
]

Performance Optimization

1. Caching Strategies

# Multi-level caching configuration
from nbviewer.cache import MemoryCache, RedisCache, FileCache

class HybridCache:
"""Multi-level cache with memory, Redis, and file backends"""

def __init__(self):
self.memory_cache = MemoryCache(max_size=100)
self.redis_cache = RedisCache(host='redis', port=6379)
self.file_cache = FileCache(cache_dir='/tmp/nbviewer_cache')

async def get(self, key):
"""Get from cache with fallback hierarchy"""
# Try memory cache first
result = await self.memory_cache.get(key)
if result:
return result

# Try Redis cache
result = await self.redis_cache.get(key)
if result:
# Store in memory cache for faster access
await self.memory_cache.set(key, result)
return result

# Try file cache
result = await self.file_cache.get(key)
if result:
# Store in Redis and memory
await self.redis_cache.set(key, result)
await self.memory_cache.set(key, result)
return result

return None

async def set(self, key, value):
"""Set in all cache levels"""
await self.memory_cache.set(key, value)
await self.redis_cache.set(key, value)
await self.file_cache.set(key, value)

c.NBViewer.cache_class = HybridCache

2. Content Delivery Network (CDN)

# CDN configuration for static assets
c.NBViewer.cdn_url = 'https://cdn.your-domain.com'

# Static file handling with CDN
c.NBViewer.static_url_prefix = 'https://cdn.your-domain.com/static/'

# Cache headers for static content
c.NBViewer.static_cache_headers = {
'Cache-Control': 'public, max-age=31536000', # 1 year
'Expires': 'Thu, 31 Dec 2024 23:59:59 GMT'
}

3. Async Processing

# Async notebook processing
import asyncio
from concurrent.futures import ThreadPoolExecutor

class AsyncNBViewer:
def __init__(self):
self.executor = ThreadPoolExecutor(max_workers=10)

async def render_notebook_async(self, notebook_path):
"""Render notebook asynchronously"""
loop = asyncio.get_event_loop()

# Run CPU-intensive rendering in thread pool
result = await loop.run_in_executor(
self.executor,
self._render_notebook_sync,
notebook_path
)

return result

def _render_notebook_sync(self, notebook_path):
"""Synchronous notebook rendering"""
# Actual rendering logic here
pass

c.NBViewer.async_rendering = True
c.NBViewer.max_workers = 10

Monitoring and Analytics

1. Metrics Collection

# Prometheus metrics integration
from prometheus_client import Counter, Histogram, Gauge
import time

# Define metrics
notebook_requests = Counter('nbviewer_requests_total', 'Total notebook requests', ['provider', 'status'])
render_duration = Histogram('nbviewer_render_duration_seconds', 'Notebook rendering duration')
cache_hit_rate = Gauge('nbviewer_cache_hit_rate', 'Cache hit rate')
active_renders = Gauge('nbviewer_active_renders', 'Number of active renders')

class MetricsHandler:
"""Collect and expose metrics"""

def __init__(self):
self.start_time = time.time()
self.total_requests = 0
self.cache_hits = 0

def record_request(self, provider, status):
"""Record notebook request"""
notebook_requests.labels(provider=provider, status=status).inc()
self.total_requests += 1

def record_render_time(self, duration):
"""Record rendering duration"""
render_duration.observe(duration)

def record_cache_hit(self):
"""Record cache hit"""
self.cache_hits += 1
cache_hit_rate.set(self.cache_hits / self.total_requests)

def update_active_renders(self, count):
"""Update active render count"""
active_renders.set(count)

# Integrate metrics with nbviewer
c.NBViewer.metrics_handler = MetricsHandler()

2. Logging Configuration

# Comprehensive logging setup
import logging
import json
from datetime import datetime

class JSONFormatter(logging.Formatter):
"""JSON log formatter for structured logging"""

def format(self, record):
log_entry = {
'timestamp': datetime.utcnow().isoformat(),
'level': record.levelname,
'message': record.getMessage(),
'module': record.module,
'function': record.funcName,
'line': record.lineno
}

# Add extra fields if present
if hasattr(record, 'notebook_path'):
log_entry['notebook_path'] = record.notebook_path
if hasattr(record, 'provider'):
log_entry['provider'] = record.provider
if hasattr(record, 'render_time'):
log_entry['render_time'] = record.render_time

return json.dumps(log_entry)

# Configure logging
c.NBViewer.log_level = 'INFO'
c.NBViewer.log_format = JSONFormatter()

# Access log configuration
c.NBViewer.access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(D)s'

Security Considerations

1. Input Validation

# Secure input validation
import re
from urllib.parse import urlparse

class SecurityValidator:
"""Validate and sanitize inputs"""

ALLOWED_SCHEMES = ['http', 'https']
BLOCKED_DOMAINS = ['localhost', '127.0.0.1', '0.0.0.0']
MAX_NOTEBOOK_SIZE = 10 * 1024 * 1024 # 10MB

def validate_url(self, url):
"""Validate notebook URL"""
try:
parsed = urlparse(url)

# Check scheme
if parsed.scheme not in self.ALLOWED_SCHEMES:
raise ValueError(f"Invalid scheme: {parsed.scheme}")

# Check for blocked domains
if parsed.hostname in self.BLOCKED_DOMAINS:
raise ValueError(f"Blocked domain: {parsed.hostname}")

# Check for private IP ranges
if self._is_private_ip(parsed.hostname):
raise ValueError(f"Private IP not allowed: {parsed.hostname}")

return True

except Exception as e:
raise ValueError(f"Invalid URL: {e}")

def validate_notebook_content(self, content):
"""Validate notebook content"""
# Check size
if len(content) > self.MAX_NOTEBOOK_SIZE:
raise ValueError("Notebook too large")

# Check for malicious content
if self._contains_malicious_content(content):
raise ValueError("Potentially malicious content detected")

return True

def _is_private_ip(self, hostname):
"""Check if hostname is a private IP"""
# Implementation for private IP detection
return False

def _contains_malicious_content(self, content):
"""Check for malicious content patterns"""
# Implementation for malicious content detection
return False

c.NBViewer.security_validator = SecurityValidator()

2. Rate Limiting

# Advanced rate limiting
from collections import defaultdict
import time

class RateLimiter:
"""Rate limiting with multiple strategies"""

def __init__(self):
self.requests = defaultdict(list)
self.blocked_ips = set()

def is_allowed(self, ip_address, endpoint):
"""Check if request is allowed"""
now = time.time()

# Check if IP is blocked
if ip_address in self.blocked_ips:
return False

# Clean old requests
self._clean_old_requests(ip_address, now)

# Get rate limits for endpoint
limits = self._get_rate_limits(endpoint)

# Check each limit
for window, max_requests in limits.items():
recent_requests = [
req_time for req_time in self.requests[ip_address]
if now - req_time < window
]

if len(recent_requests) >= max_requests:
# Block IP if too many requests
if len(recent_requests) > max_requests * 2:
self.blocked_ips.add(ip_address)
return False

# Record this request
self.requests[ip_address].append(now)
return True

def _clean_old_requests(self, ip_address, now):
"""Remove old request records"""
self.requests[ip_address] = [
req_time for req_time in self.requests[ip_address]
if now - req_time < 3600 # Keep 1 hour of history
]

def _get_rate_limits(self, endpoint):
"""Get rate limits for endpoint"""
if endpoint.startswith('/github/'):
return {60: 30, 3600: 200} # 30/min, 200/hour
elif endpoint.startswith('/gist/'):
return {60: 20, 3600: 100} # 20/min, 100/hour
else:
return {60: 10, 3600: 50} # 10/min, 50/hour

c.NBViewer.rate_limiter = RateLimiter()

Best Practices

1. Performance Best Practices

# Optimized nbviewer configuration
c.NBViewer.cache_expiry_min = 300 # 5 minutes minimum cache
c.NBViewer.cache_expiry_max = 3600 # 1 hour maximum cache
c.NBViewer.max_cache_size = 1000 # Maximum cached notebooks

# Async processing
c.NBViewer.async_rendering = True
c.NBViewer.max_concurrent_renders = 10

# Content optimization
c.NBViewer.compress_output = True
c.NBViewer.minify_html = True
c.NBViewer.optimize_images = True

# CDN configuration
c.NBViewer.use_cdn = True
c.NBViewer.cdn_url = 'https://cdn.example.com'

2. Security Best Practices

# Security configuration
c.NBViewer.enable_csp = True # Content Security Policy
c.NBViewer.csp_policy = {
'default-src': "'self'",
'script-src': "'self' 'unsafe-inline'",
'style-src': "'self' 'unsafe-inline'",
'img-src': "'self' data: https:",
'font-src': "'self' https:",
}

# Input sanitization
c.NBViewer.sanitize_html = True
c.NBViewer.allow_iframe_embedding = False
c.NBViewer.max_notebook_size = 10 * 1024 * 1024 # 10MB

# Rate limiting
c.NBViewer.enable_rate_limiting = True
c.NBViewer.rate_limit_per_ip = 60 # requests per minute

3. Operational Best Practices

# Monitoring and alerting
c.NBViewer.enable_metrics = True
c.NBViewer.metrics_port = 9090

# Health checks
c.NBViewer.health_check_endpoint = '/health'
c.NBViewer.ready_check_endpoint = '/ready'

# Graceful shutdown
c.NBViewer.shutdown_timeout = 30 # seconds

# Error handling
c.NBViewer.error_template = 'custom_error.html'
c.NBViewer.show_error_details = False # Don't expose internals

Troubleshooting

Common Issues and Solutions

1. Rendering Failures

# Debug rendering issues
import logging

# Enable debug logging
c.NBViewer.log_level = 'DEBUG'

# Custom error handler
def handle_render_error(notebook_path, error):
"""Handle notebook rendering errors"""
logging.error(f"Failed to render {notebook_path}: {error}")

# Try alternative rendering methods
if "timeout" in str(error).lower():
# Increase timeout and retry
return retry_with_timeout(notebook_path, timeout=300)
elif "memory" in str(error).lower():
# Use streaming renderer for large notebooks
return stream_render(notebook_path)
else:
# Return error page
return render_error_page(error)

c.NBViewer.error_handler = handle_render_error

2. Performance Issues

# Monitor performance
# Check memory usage
docker stats nbviewer

# Check cache hit rate
curl http://localhost:8080/metrics | grep cache_hit_rate

# Check response times
curl -w "@curl-format.txt" -o /dev/null -s "http://localhost:8080/github/user/repo/blob/main/notebook.ipynb"

3. Cache Issues

# Cache debugging
def debug_cache_issues():
"""Debug cache-related problems"""

# Check cache connectivity
try:
cache = get_cache_instance()
cache.set('test_key', 'test_value')
value = cache.get('test_key')
assert value == 'test_value'
print("Cache connectivity: OK")
except Exception as e:
print(f"Cache connectivity: FAILED - {e}")

# Check cache size
cache_size = cache.size()
print(f"Cache size: {cache_size} items")

# Check cache hit rate
hit_rate = cache.get_hit_rate()
print(f"Cache hit rate: {hit_rate:.2%}")

# Run cache diagnostics
debug_cache_issues()

Conclusion

nbviewer provides an essential service for the Jupyter ecosystem by enabling easy sharing and viewing of notebooks. Key benefits include:

Key Benefits

  • Easy Sharing: Simple URL-based notebook sharing
  • No Installation: Viewers don't need Jupyter installed
  • Multiple Sources: Support for GitHub, Gist, URLs, and uploads
  • Customizable: Self-hostable with extensive customization options

Best Use Cases

  • Documentation: Share analysis and tutorials
  • Education: Distribute course materials and assignments
  • Collaboration: Share research and findings with teams
  • Portfolio: Showcase data science projects

When to Choose nbviewer

  • Need to share notebooks publicly
  • Want read-only notebook viewing
  • Require embedding in websites
  • Need custom branding and theming

nbviewer bridges the gap between notebook creators and consumers, making Jupyter notebooks accessible to a broader audience without technical barriers.

Resources

Official Resources

Community Resources

  • JupyterHub Setup
  • Apache Spark Guide
  • Apache Livy Guide