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
.ipynbfiles 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>© 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
Related Documentation
- JupyterHub Setup
- Apache Spark Guide
- Apache Livy Guide