- Add ADR-014: RSS Feed Implementation - Add ADR-015: Phase 5 Implementation Approach - Add Phase 5 design documents (RSS and container) - Add pre-implementation review - Add RSS and container validation reports - Add architectural approval for v0.6.0 release Architecture reviews confirm 98/100 (RSS) and 96/100 (container) scores. Phase 5 approved for production deployment. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1258 lines
36 KiB
Markdown
1258 lines
36 KiB
Markdown
# Phase 5: RSS Feed & Production Container - Implementation Design
|
|
|
|
**Date**: 2025-11-18
|
|
**Phase**: 5
|
|
**Status**: Design Complete, Ready for Implementation
|
|
**Dependencies**: Phase 4 (Web Interface) complete
|
|
**Version**: 0.6.0 target
|
|
|
|
## Implementation Guidelines
|
|
|
|
### Version Management
|
|
- **Current Version**: 0.5.1 (in `starpunk/__init__.py`)
|
|
- **Target Version**: 0.6.0 (skip intermediate versions)
|
|
- **Rationale**: Phase 5 introduces significant new features warranting minor version bump
|
|
|
|
### Git Workflow
|
|
- **Branch Strategy**: Use feature branch `feature/phase-5-rss-container`
|
|
- **Workflow**:
|
|
1. Create feature branch from main
|
|
2. Implement all Phase 5 features
|
|
3. Create PR for review
|
|
4. Merge to main when complete
|
|
- **Rationale**: Cleaner history, easier rollback, follows project standards for larger features
|
|
|
|
## Executive Summary
|
|
|
|
Phase 5 implements RSS feed generation with full IndieWeb compliance and creates a production-ready container for deployment testing. This phase completes the core V1 feature set for content syndication and enables testing of IndieAuth authentication in a production-like environment.
|
|
|
|
**Key Deliverables**:
|
|
- RSS 2.0 feed generation with proper microformats
|
|
- Feed route and caching strategy
|
|
- Production-ready container (Podman/Docker compatible)
|
|
- Container configuration for public-facing deployment
|
|
- IndieAuth testing capability with HTTPS
|
|
- Container orchestration for development and production
|
|
|
|
## Scope
|
|
|
|
### In Scope for Phase 5
|
|
|
|
**RSS Feed Generation**:
|
|
- RSS 2.0 compliant XML feed
|
|
- Recent published notes (configurable limit)
|
|
- Proper RFC-822 date formatting
|
|
- HTML content with CDATA wrapping
|
|
- Unique GUID for each entry
|
|
- Channel metadata (title, link, description)
|
|
- Feed caching (5-minute default)
|
|
- Auto-discovery link in templates
|
|
|
|
**Production Container**:
|
|
- Containerfile (Podman/Docker compatible)
|
|
- Multi-stage build for optimization
|
|
- Gunicorn WSGI server configuration
|
|
- Health check endpoint
|
|
- Volume mounts for persistent data
|
|
- Environment variable configuration
|
|
- Production-ready security settings
|
|
- Container compose configuration
|
|
|
|
**Deployment Features**:
|
|
- HTTPS support via reverse proxy
|
|
- IndieAuth callback handling
|
|
- Production logging configuration
|
|
- Graceful shutdown handling
|
|
- Data persistence strategy
|
|
- Container networking setup
|
|
|
|
### Out of Scope
|
|
|
|
**Deferred to Phase 6+**:
|
|
- Micropub endpoint implementation
|
|
- Feed pagination
|
|
- Feed filtering/categories
|
|
- Atom feed format
|
|
- JSON Feed format
|
|
- Feed validation UI
|
|
- Auto-update notification
|
|
- Feed analytics
|
|
- Multiple feed types
|
|
- Advanced caching strategies
|
|
|
|
## Architecture Overview
|
|
|
|
### Component Diagram
|
|
|
|
```
|
|
┌────────────────────────────────────────────────────────┐
|
|
│ Client (Browser/RSS Reader) │
|
|
└────────────┬───────────────────────────────────────────┘
|
|
│
|
|
│ HTTPS (port 443)
|
|
↓
|
|
┌────────────────────────────────────────────────────────┐
|
|
│ Reverse Proxy (Caddy/Nginx) - Host │
|
|
│ - SSL Termination │
|
|
│ - Proxy to container │
|
|
└────────────┬───────────────────────────────────────────┘
|
|
│
|
|
│ HTTP (port 8000)
|
|
↓
|
|
┌────────────────────────────────────────────────────────┐
|
|
│ Container (starpunk:0.6.0) │
|
|
│ ┌────────────────────────────────────────────────────┐ │
|
|
│ │ Gunicorn WSGI Server (4 workers) │ │
|
|
│ └─────────────┬──────────────────────────────────────┘ │
|
|
│ │ │
|
|
│ ↓ │
|
|
│ ┌────────────────────────────────────────────────────┐ │
|
|
│ │ Flask Application │ │
|
|
│ │ ┌──────────────┐ ┌──────────────┐ │ │
|
|
│ │ │ Web Routes │ │ Feed Route │ │ │
|
|
│ │ │ (existing) │ │ /feed.xml │ │ │
|
|
│ │ └──────────────┘ └──────┬───────┘ │ │
|
|
│ │ │ │ │
|
|
│ │ ↓ │ │
|
|
│ │ ┌──────────────────┐ │ │
|
|
│ │ │ Feed Generator │ │ │
|
|
│ │ │ (new module) │ │ │
|
|
│ │ └──────┬───────────┘ │ │
|
|
│ └───────────────────────────┼────────────────────────┘ │
|
|
│ │ │
|
|
│ ↓ │
|
|
│ ┌────────────────────────────────────────────────────┐ │
|
|
│ │ Business Logic (Notes Module) │ │
|
|
│ └─────────────┬──────────────────────────────────────┘ │
|
|
│ │ │
|
|
│ ↓ │
|
|
│ ┌────────────────────────────────────────────────────┐ │
|
|
│ │ Data Layer (Volume Mount) │ │
|
|
│ │ ┌──────────────┐ ┌────────────────┐ │ │
|
|
│ │ │ Markdown │ │ SQLite DB │ │ │
|
|
│ │ │ Files │ │ starpunk.db │ │ │
|
|
│ │ └──────────────┘ └────────────────┘ │ │
|
|
│ └────────────────────────────────────────────────────┘ │
|
|
│ /data (bind mount to host) │
|
|
└────────────────────────────────────────────────────────┘
|
|
│
|
|
↓
|
|
┌────────────────────────────────────────────────────────┐
|
|
│ Host Filesystem (Persistent Data) │
|
|
│ ./container-data/ │
|
|
│ ├── notes/ (markdown files) │
|
|
│ └── starpunk.db (database) │
|
|
└────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### Feed Generation Flow
|
|
|
|
```
|
|
RSS Reader → GET /feed.xml
|
|
↓
|
|
Route Handler → Check cache (5 min)
|
|
↓
|
|
Cache Hit? → Yes → Return cached XML
|
|
↓ No
|
|
Feed Generator → list_notes(published=True, limit=50)
|
|
↓
|
|
Notes Module → Query database + read files
|
|
↓
|
|
Feed Generator → Build RSS XML:
|
|
- Channel metadata
|
|
- For each note:
|
|
- Title (first line or timestamp)
|
|
- Link (permalink)
|
|
- Description (HTML content)
|
|
- PubDate (RFC-822 format)
|
|
- GUID (permalink)
|
|
↓
|
|
Cache XML (5 minutes)
|
|
↓
|
|
RSS Reader ← Return application/rss+xml
|
|
```
|
|
|
|
## RSS Feed Specification
|
|
|
|
### Module: `starpunk/feed.py`
|
|
|
|
**Purpose**: Generate RSS 2.0 compliant feed from published notes
|
|
|
|
**Functions**:
|
|
```python
|
|
def generate_feed(site_url: str, site_name: str, site_description: str,
|
|
notes: list[Note], limit: int = 50) -> str:
|
|
"""
|
|
Generate RSS 2.0 XML feed
|
|
|
|
Args:
|
|
site_url: Base URL of the site
|
|
site_name: Site title for the feed
|
|
site_description: Site description
|
|
notes: List of Note objects to include
|
|
limit: Maximum number of entries (default 50)
|
|
|
|
Returns:
|
|
RSS 2.0 XML string
|
|
|
|
Raises:
|
|
ValueError: If site_url or site_name is empty
|
|
"""
|
|
|
|
def format_rfc822_date(dt: datetime) -> str:
|
|
"""
|
|
Format datetime to RFC-822 format for RSS
|
|
|
|
Args:
|
|
dt: Datetime object to format
|
|
|
|
Returns:
|
|
RFC-822 formatted date string
|
|
Example: "Mon, 18 Nov 2024 12:00:00 +0000"
|
|
"""
|
|
|
|
def get_note_title(note: Note) -> str:
|
|
"""
|
|
Extract title from note content (first line or timestamp)
|
|
|
|
Args:
|
|
note: Note object
|
|
|
|
Returns:
|
|
Title string (max 100 chars)
|
|
"""
|
|
|
|
def clean_html_for_rss(html: str) -> str:
|
|
"""
|
|
Ensure HTML is safe for RSS CDATA wrapping
|
|
|
|
Args:
|
|
html: Rendered HTML content
|
|
|
|
Returns:
|
|
Cleaned HTML safe for CDATA
|
|
"""
|
|
```
|
|
|
|
### RSS Feed Structure
|
|
|
|
**Channel Elements**:
|
|
```xml
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
|
<channel>
|
|
<title>Site Name</title>
|
|
<link>https://example.com/</link>
|
|
<description>Site description</description>
|
|
<language>en-us</language>
|
|
<lastBuildDate>Mon, 18 Nov 2024 12:00:00 +0000</lastBuildDate>
|
|
<atom:link href="https://example.com/feed.xml" rel="self" type="application/rss+xml"/>
|
|
|
|
<!-- Items -->
|
|
<item>
|
|
<title>Note Title or Timestamp</title>
|
|
<link>https://example.com/note/my-note-slug</link>
|
|
<guid isPermaLink="true">https://example.com/note/my-note-slug</guid>
|
|
<pubDate>Mon, 18 Nov 2024 10:30:00 +0000</pubDate>
|
|
<description><![CDATA[
|
|
<p>Rendered HTML content goes here</p>
|
|
]]></description>
|
|
</item>
|
|
|
|
<!-- More items... -->
|
|
</channel>
|
|
</rss>
|
|
```
|
|
|
|
**Standards Compliance**:
|
|
- RSS 2.0 specification
|
|
- RFC-822 date format
|
|
- CDATA wrapping for HTML content
|
|
- Atom self-link for feed discovery
|
|
- Proper GUID (permalink as GUID)
|
|
- XML declaration with UTF-8 encoding
|
|
|
|
### Route: `GET /feed.xml`
|
|
|
|
**Handler**: `public.feed()`
|
|
|
|
**Location**: Add to `starpunk/routes/public.py`
|
|
|
|
**Implementation**:
|
|
```python
|
|
from flask import Response, current_app
|
|
from starpunk.feed import generate_feed
|
|
from starpunk.notes import list_notes
|
|
import hashlib
|
|
|
|
# Simple in-memory cache
|
|
_feed_cache = {'xml': None, 'timestamp': None, 'etag': None}
|
|
|
|
@bp.route("/feed.xml")
|
|
def feed():
|
|
"""
|
|
RSS 2.0 feed of published notes
|
|
|
|
Returns:
|
|
XML response with RSS feed
|
|
|
|
Headers:
|
|
Content-Type: application/rss+xml; charset=utf-8
|
|
Cache-Control: public, max-age=300
|
|
ETag: hash of feed content
|
|
|
|
Caching:
|
|
- Server-side: 5 minute in-memory cache
|
|
- Client-side: 5 minute cache via Cache-Control
|
|
- Conditional requests: ETag support
|
|
"""
|
|
from datetime import datetime, timedelta
|
|
|
|
# Check if cache is valid (5 minutes)
|
|
cache_duration = timedelta(minutes=5)
|
|
now = datetime.utcnow()
|
|
|
|
if _feed_cache['xml'] and _feed_cache['timestamp']:
|
|
if now - _feed_cache['timestamp'] < cache_duration:
|
|
# Return cached feed with ETag
|
|
response = Response(
|
|
_feed_cache['xml'],
|
|
mimetype='application/rss+xml; charset=utf-8'
|
|
)
|
|
response.headers['Cache-Control'] = 'public, max-age=300'
|
|
response.headers['ETag'] = _feed_cache['etag']
|
|
return response
|
|
|
|
# Generate fresh feed
|
|
notes = list_notes(published_only=True, limit=50)
|
|
|
|
feed_xml = generate_feed(
|
|
site_url=current_app.config['SITE_URL'],
|
|
site_name=current_app.config['SITE_NAME'],
|
|
site_description=current_app.config.get('SITE_DESCRIPTION', ''),
|
|
notes=notes
|
|
)
|
|
|
|
# Calculate ETag
|
|
etag = hashlib.md5(feed_xml.encode('utf-8')).hexdigest()
|
|
|
|
# Update cache
|
|
_feed_cache['xml'] = feed_xml
|
|
_feed_cache['timestamp'] = now
|
|
_feed_cache['etag'] = etag
|
|
|
|
# Return response
|
|
response = Response(feed_xml, mimetype='application/rss+xml; charset=utf-8')
|
|
response.headers['Cache-Control'] = 'public, max-age=300'
|
|
response.headers['ETag'] = etag
|
|
|
|
return response
|
|
```
|
|
|
|
### Template Updates
|
|
|
|
**Add to `templates/base.html`**:
|
|
```html
|
|
<head>
|
|
<!-- Existing head content -->
|
|
|
|
<!-- RSS Feed Discovery -->
|
|
<link rel="alternate" type="application/rss+xml"
|
|
title="{{ config.SITE_NAME }} RSS Feed"
|
|
href="{{ url_for('public.feed', _external=True) }}">
|
|
</head>
|
|
```
|
|
|
|
**Add feed link to homepage** (`templates/index.html`):
|
|
```html
|
|
<nav>
|
|
<a href="/">Home</a>
|
|
<a href="{{ url_for('public.feed') }}">RSS Feed</a>
|
|
</nav>
|
|
```
|
|
|
|
## Production Container Specification
|
|
|
|
### Containerfile (Podman/Docker Compatible)
|
|
|
|
**Location**: `/home/phil/Projects/starpunk/Containerfile`
|
|
|
|
**Purpose**: Multi-stage production-optimized container
|
|
|
|
**Contents**:
|
|
```dockerfile
|
|
# syntax=docker/dockerfile:1
|
|
|
|
# Build stage - minimal Python build environment
|
|
FROM python:3.11-slim AS builder
|
|
|
|
# Install uv for fast dependency installation
|
|
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
|
|
|
WORKDIR /build
|
|
|
|
# Copy dependency files
|
|
COPY requirements.txt .
|
|
|
|
# Create virtual environment and install dependencies
|
|
RUN uv venv /opt/venv && \
|
|
. /opt/venv/bin/activate && \
|
|
uv pip install --no-cache -r requirements.txt
|
|
|
|
# Runtime stage - minimal runtime image
|
|
FROM python:3.11-slim
|
|
|
|
# Create non-root user for security
|
|
RUN useradd --create-home --shell /bin/bash starpunk && \
|
|
mkdir -p /app /data/notes && \
|
|
chown -R starpunk:starpunk /app /data
|
|
|
|
# Copy virtual environment from builder
|
|
COPY --from=builder /opt/venv /opt/venv
|
|
|
|
# Set PATH to use venv
|
|
ENV PATH="/opt/venv/bin:$PATH" \
|
|
PYTHONUNBUFFERED=1 \
|
|
PYTHONDONTWRITEBYTECODE=1
|
|
|
|
WORKDIR /app
|
|
|
|
# Copy application code
|
|
COPY --chown=starpunk:starpunk . .
|
|
|
|
# Switch to non-root user
|
|
USER starpunk
|
|
|
|
# Expose application port
|
|
EXPOSE 8000
|
|
|
|
# Health check
|
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
CMD python3 -c "import httpx; httpx.get('http://localhost:8000/health', timeout=2.0)" || exit 1
|
|
|
|
# Run gunicorn
|
|
CMD ["gunicorn", \
|
|
"--bind", "0.0.0.0:8000", \
|
|
"--workers", "4", \
|
|
"--worker-class", "sync", \
|
|
"--worker-tmp-dir", "/dev/shm", \
|
|
"--max-requests", "1000", \
|
|
"--max-requests-jitter", "50", \
|
|
"--timeout", "30", \
|
|
"--graceful-timeout", "30", \
|
|
"--access-logfile", "-", \
|
|
"--error-logfile", "-", \
|
|
"--log-level", "info", \
|
|
"app:app"]
|
|
```
|
|
|
|
### Container Compose Configuration
|
|
|
|
**Location**: `/home/phil/Projects/starpunk/compose.yaml`
|
|
|
|
**Purpose**: Production deployment orchestration
|
|
|
|
**Contents**:
|
|
```yaml
|
|
version: '3.8'
|
|
|
|
services:
|
|
starpunk:
|
|
image: starpunk:0.6.0
|
|
container_name: starpunk
|
|
build:
|
|
context: .
|
|
dockerfile: Containerfile
|
|
|
|
# Restart policy
|
|
restart: unless-stopped
|
|
|
|
# Ports - only expose to localhost
|
|
ports:
|
|
- "127.0.0.1:8000:8000"
|
|
|
|
# Environment variables
|
|
env_file:
|
|
- .env
|
|
|
|
environment:
|
|
# Override .env for container environment
|
|
- FLASK_ENV=production
|
|
- FLASK_DEBUG=0
|
|
- DATA_PATH=/data
|
|
- NOTES_PATH=/data/notes
|
|
- DATABASE_PATH=/data/starpunk.db
|
|
|
|
# Volume mounts for persistent data
|
|
volumes:
|
|
- ./container-data:/data:rw
|
|
|
|
# Health check
|
|
healthcheck:
|
|
test: ["CMD", "python3", "-c", "import httpx; httpx.get('http://localhost:8000/health', timeout=2.0)"]
|
|
interval: 30s
|
|
timeout: 3s
|
|
retries: 3
|
|
start_period: 10s
|
|
|
|
# Resource limits
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
cpus: '1.0'
|
|
memory: 512M
|
|
reservations:
|
|
cpus: '0.25'
|
|
memory: 128M
|
|
|
|
# Logging
|
|
logging:
|
|
driver: "json-file"
|
|
options:
|
|
max-size: "10m"
|
|
max-file: "3"
|
|
|
|
# Network
|
|
networks:
|
|
- starpunk-net
|
|
|
|
networks:
|
|
starpunk-net:
|
|
driver: bridge
|
|
```
|
|
|
|
### Health Check Endpoint
|
|
|
|
**Add to `starpunk/__init__.py`**:
|
|
```python
|
|
@app.route('/health')
|
|
def health_check():
|
|
"""
|
|
Health check endpoint for containers and monitoring
|
|
|
|
Returns:
|
|
JSON with status and basic info
|
|
|
|
Response codes:
|
|
200: Application healthy
|
|
500: Application unhealthy
|
|
|
|
Checks:
|
|
- Database connectivity
|
|
- File system access
|
|
- Basic application state
|
|
"""
|
|
from flask import jsonify
|
|
import os
|
|
|
|
try:
|
|
# Check database
|
|
from starpunk.database import get_db
|
|
with app.app_context():
|
|
db = get_db(app)
|
|
db.execute("SELECT 1").fetchone()
|
|
db.close()
|
|
|
|
# Check file system
|
|
data_path = app.config['DATA_PATH']
|
|
if not os.path.exists(data_path):
|
|
raise Exception("Data path not accessible")
|
|
|
|
return jsonify({
|
|
'status': 'healthy',
|
|
'version': app.config.get('VERSION', '0.6.0'),
|
|
'environment': app.config.get('ENV', 'unknown')
|
|
}), 200
|
|
|
|
except Exception as e:
|
|
return jsonify({
|
|
'status': 'unhealthy',
|
|
'error': str(e)
|
|
}), 500
|
|
```
|
|
|
|
### Container Build & Run Instructions
|
|
|
|
**Build container**:
|
|
```bash
|
|
# Using Docker
|
|
docker build -t starpunk:0.6.0 -f Containerfile .
|
|
|
|
# Using Podman
|
|
podman build -t starpunk:0.6.0 -f Containerfile .
|
|
```
|
|
|
|
**Run container (development)**:
|
|
```bash
|
|
# Create data directory
|
|
mkdir -p container-data/notes
|
|
|
|
# Run with docker
|
|
docker run -d \
|
|
--name starpunk \
|
|
-p 127.0.0.1:8000:8000 \
|
|
-v $(pwd)/container-data:/data:rw \
|
|
--env-file .env \
|
|
starpunk:0.6.0
|
|
|
|
# Run with podman
|
|
podman run -d \
|
|
--name starpunk \
|
|
-p 127.0.0.1:8000:8000 \
|
|
-v $(pwd)/container-data:/data:rw,Z \
|
|
--env-file .env \
|
|
starpunk:0.6.0
|
|
```
|
|
|
|
**Run with compose**:
|
|
```bash
|
|
# Docker Compose
|
|
docker compose up -d
|
|
|
|
# Podman Compose
|
|
podman-compose up -d
|
|
```
|
|
|
|
### Reverse Proxy Configuration
|
|
|
|
**Caddy** (recommended for auto-HTTPS):
|
|
|
|
**Location**: `/home/phil/Projects/starpunk/Caddyfile.example`
|
|
|
|
```caddy
|
|
# Caddyfile for StarPunk reverse proxy
|
|
# Rename to Caddyfile and update domain
|
|
|
|
your-domain.com {
|
|
# Reverse proxy to container
|
|
reverse_proxy localhost:8000
|
|
|
|
# Logging
|
|
log {
|
|
output file /var/log/caddy/starpunk.log
|
|
}
|
|
|
|
# Security headers
|
|
header {
|
|
# Remove server header
|
|
-Server
|
|
|
|
# Security headers
|
|
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
|
X-Content-Type-Options "nosniff"
|
|
X-Frame-Options "DENY"
|
|
X-XSS-Protection "1; mode=block"
|
|
Referrer-Policy "strict-origin-when-cross-origin"
|
|
}
|
|
|
|
# Compress responses
|
|
encode gzip zstd
|
|
|
|
# Static file caching
|
|
@static {
|
|
path /static/*
|
|
}
|
|
header @static Cache-Control "public, max-age=31536000, immutable"
|
|
}
|
|
```
|
|
|
|
**Nginx** (alternative):
|
|
|
|
**Location**: `/home/phil/Projects/starpunk/nginx.conf.example`
|
|
|
|
```nginx
|
|
# Nginx configuration for StarPunk
|
|
# Save to /etc/nginx/sites-available/starpunk
|
|
|
|
upstream starpunk {
|
|
server localhost:8000;
|
|
}
|
|
|
|
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 certificates (use certbot)
|
|
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
|
|
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
|
|
|
|
# SSL configuration
|
|
ssl_protocols TLSv1.2 TLSv1.3;
|
|
ssl_ciphers HIGH:!aNULL:!MD5;
|
|
ssl_prefer_server_ciphers on;
|
|
|
|
# Security headers
|
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
add_header X-Frame-Options "DENY" always;
|
|
add_header X-XSS-Protection "1; mode=block" always;
|
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
|
|
# Logging
|
|
access_log /var/log/nginx/starpunk-access.log;
|
|
error_log /var/log/nginx/starpunk-error.log;
|
|
|
|
# Max upload size
|
|
client_max_body_size 10M;
|
|
|
|
# Proxy to container
|
|
location / {
|
|
proxy_pass http://starpunk;
|
|
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;
|
|
|
|
# Timeouts
|
|
proxy_connect_timeout 30s;
|
|
proxy_send_timeout 30s;
|
|
proxy_read_timeout 30s;
|
|
}
|
|
|
|
# Static files caching
|
|
location /static/ {
|
|
proxy_pass http://starpunk;
|
|
proxy_cache_valid 200 1y;
|
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
|
}
|
|
|
|
# RSS feed caching
|
|
location /feed.xml {
|
|
proxy_pass http://starpunk;
|
|
proxy_cache_valid 200 5m;
|
|
add_header Cache-Control "public, max-age=300";
|
|
}
|
|
}
|
|
```
|
|
|
|
## Configuration Updates
|
|
|
|
### Environment Variables
|
|
|
|
**Add to `.env.example`**:
|
|
```bash
|
|
# =============================================================================
|
|
# RSS FEED CONFIGURATION
|
|
# =============================================================================
|
|
|
|
# Maximum number of items in RSS feed (default: 50)
|
|
FEED_MAX_ITEMS=50
|
|
|
|
# Feed cache duration in seconds (default: 300 = 5 minutes)
|
|
FEED_CACHE_SECONDS=300
|
|
|
|
# =============================================================================
|
|
# CONTAINER CONFIGURATION
|
|
# =============================================================================
|
|
|
|
# Application version
|
|
VERSION=0.6.0
|
|
|
|
# Environment: development or production
|
|
ENVIRONMENT=production
|
|
|
|
# Number of Gunicorn workers (default: 4)
|
|
# Recommendation: (2 x CPU cores) + 1
|
|
WORKERS=4
|
|
|
|
# Worker timeout in seconds (default: 30)
|
|
WORKER_TIMEOUT=30
|
|
|
|
# Max requests per worker before restart (prevents memory leaks)
|
|
MAX_REQUESTS=1000
|
|
```
|
|
|
|
### Config Module Updates
|
|
|
|
**Update `starpunk/config.py`**:
|
|
```python
|
|
# RSS feed configuration
|
|
app.config['FEED_MAX_ITEMS'] = int(os.getenv('FEED_MAX_ITEMS', '50'))
|
|
app.config['FEED_CACHE_SECONDS'] = int(os.getenv('FEED_CACHE_SECONDS', '300'))
|
|
|
|
# Application version
|
|
app.config['VERSION'] = os.getenv('VERSION', '0.6.0')
|
|
|
|
# Container/deployment configuration
|
|
app.config['ENVIRONMENT'] = os.getenv('ENVIRONMENT', 'development')
|
|
app.config['WORKERS'] = int(os.getenv('WORKERS', '4'))
|
|
```
|
|
|
|
## Testing Strategy
|
|
|
|
### Unit Tests
|
|
|
|
**File**: `tests/test_feed.py`
|
|
|
|
**Tests**:
|
|
```python
|
|
def test_generate_feed_basic():
|
|
"""Test feed generation with minimal notes"""
|
|
|
|
def test_generate_feed_empty():
|
|
"""Test feed generation with no notes"""
|
|
|
|
def test_generate_feed_limit():
|
|
"""Test feed respects item limit"""
|
|
|
|
def test_rfc822_date_format():
|
|
"""Test RFC-822 date formatting"""
|
|
|
|
def test_note_title_extraction():
|
|
"""Test title extraction from note content"""
|
|
|
|
def test_html_cleaning_for_cdata():
|
|
"""Test HTML cleaning for CDATA wrapping"""
|
|
|
|
def test_feed_xml_structure():
|
|
"""Test XML structure is valid"""
|
|
|
|
def test_feed_includes_all_required_elements():
|
|
"""Test all required RSS elements present"""
|
|
```
|
|
|
|
**File**: `tests/test_routes_feed.py`
|
|
|
|
**Tests**:
|
|
```python
|
|
def test_feed_route_returns_xml():
|
|
"""Test /feed.xml returns XML"""
|
|
|
|
def test_feed_route_content_type():
|
|
"""Test correct Content-Type header"""
|
|
|
|
def test_feed_route_caching():
|
|
"""Test feed caching behavior"""
|
|
|
|
def test_feed_route_etag():
|
|
"""Test ETag generation and validation"""
|
|
|
|
def test_feed_route_only_published():
|
|
"""Test feed only includes published notes"""
|
|
|
|
def test_feed_route_limit():
|
|
"""Test feed respects limit configuration"""
|
|
```
|
|
|
|
### Integration Tests
|
|
|
|
**Container Testing**:
|
|
```bash
|
|
#!/bin/bash
|
|
# Test container build and basic functionality
|
|
|
|
# Build container
|
|
podman build -t starpunk:0.6.0-test -f Containerfile .
|
|
|
|
# Run container
|
|
podman run -d \
|
|
--name starpunk-test \
|
|
-p 8000:8000 \
|
|
--env-file .env.test \
|
|
starpunk:0.6.0-test
|
|
|
|
# Wait for startup
|
|
sleep 5
|
|
|
|
# Test health endpoint
|
|
curl -f http://localhost:8000/health || exit 1
|
|
|
|
# Test feed endpoint
|
|
curl -f http://localhost:8000/feed.xml || exit 1
|
|
|
|
# Check feed is valid XML
|
|
curl -s http://localhost:8000/feed.xml | xmllint --noout - || exit 1
|
|
|
|
# Cleanup
|
|
podman stop starpunk-test
|
|
podman rm starpunk-test
|
|
|
|
echo "Container tests passed!"
|
|
```
|
|
|
|
### Manual Validation
|
|
|
|
**RSS Validation**:
|
|
1. Visit https://validator.w3.org/feed/
|
|
2. Enter feed URL
|
|
3. Verify no errors
|
|
4. Check all required elements present
|
|
|
|
**IndieAuth Testing**:
|
|
1. Deploy container with public HTTPS URL
|
|
2. Configure ADMIN_ME with your domain
|
|
3. Test IndieLogin authentication flow
|
|
4. Verify callback works correctly
|
|
5. Test session creation and validation
|
|
|
|
**Feed Reader Testing**:
|
|
1. Add feed to RSS reader (Feedly, NewsBlur, etc.)
|
|
2. Verify notes appear correctly
|
|
3. Check formatting and links
|
|
4. Test update behavior
|
|
|
|
## File Organization
|
|
|
|
```
|
|
starpunk/
|
|
├── feed.py # RSS feed generation (new)
|
|
├── routes/
|
|
│ └── public.py # Add feed route (update)
|
|
├── __init__.py # Add health check (update)
|
|
└── config.py # Add feed config (update)
|
|
|
|
/home/phil/Projects/starpunk/ (root)
|
|
├── Containerfile # Container build (new)
|
|
├── compose.yaml # Container orchestration (new)
|
|
├── Caddyfile.example # Caddy config (new)
|
|
├── nginx.conf.example # Nginx config (new)
|
|
├── .containerignore # Container build ignore (new)
|
|
└── .env.example # Update with feed/container config
|
|
|
|
templates/
|
|
├── base.html # Add RSS discovery link (update)
|
|
└── index.html # Add RSS link in nav (update)
|
|
|
|
tests/
|
|
├── test_feed.py # Feed generation tests (new)
|
|
└── test_routes_feed.py # Feed route tests (new)
|
|
|
|
docs/
|
|
├── designs/
|
|
│ ├── phase-5-rss-and-container.md # This file
|
|
│ └── phase-5-quick-reference.md # Implementation guide
|
|
└── decisions/
|
|
└── ADR-014-rss-feed-implementation.md # RSS decision record
|
|
```
|
|
|
|
## Security Considerations
|
|
|
|
### Feed Security
|
|
- No authentication required (public feed)
|
|
- Only published notes included
|
|
- HTML content sanitized via markdown rendering
|
|
- CDATA wrapping prevents XSS
|
|
- No user input in feed generation
|
|
- Rate limiting via reverse proxy
|
|
|
|
### Container Security
|
|
- Non-root user (starpunk:starpunk)
|
|
- Minimal base image (python:3.11-slim)
|
|
- No unnecessary packages
|
|
- Read-only root filesystem (optional)
|
|
- Resource limits (CPU, memory)
|
|
- Health checks for failure detection
|
|
- Secrets via environment variables (not in image)
|
|
- Volume permissions (UID/GID mapping)
|
|
|
|
### Production Deployment
|
|
- HTTPS required (via reverse proxy)
|
|
- Security headers enforced
|
|
- Session cookies: Secure, HttpOnly, SameSite
|
|
- CSRF protection maintained
|
|
- No debug mode in production
|
|
- Logging without sensitive data
|
|
- Regular security updates (base image)
|
|
|
|
## Performance Considerations
|
|
|
|
### RSS Feed
|
|
- Server-side caching (5 minutes)
|
|
- Client-side caching (Cache-Control)
|
|
- Conditional requests (ETag)
|
|
- Limit feed items (default 50)
|
|
- Efficient database query
|
|
- Pre-rendered HTML (no render on feed generation)
|
|
|
|
### Container Optimization
|
|
- Multi-stage build (smaller image)
|
|
- Gunicorn with multiple workers
|
|
- Worker recycling (max-requests)
|
|
- Shared memory for worker tmp
|
|
- Minimal logging overhead
|
|
- Resource limits prevent resource exhaustion
|
|
|
|
### Expected Performance
|
|
- Feed generation: < 100ms (cached: < 10ms)
|
|
- Container startup: < 5 seconds
|
|
- Memory usage: < 256MB (typical)
|
|
- CPU usage: < 10% (idle), < 50% (load)
|
|
- Container image size: < 200MB
|
|
|
|
## Migration from Phase 4
|
|
|
|
### Code Changes
|
|
- Add `starpunk/feed.py` module
|
|
- Update `starpunk/routes/public.py` (add feed route)
|
|
- Update `starpunk/__init__.py` (add health check)
|
|
- Update `starpunk/config.py` (add feed config)
|
|
- Update templates (RSS discovery links)
|
|
|
|
### New Files
|
|
- `Containerfile`
|
|
- `compose.yaml`
|
|
- `Caddyfile.example`
|
|
- `nginx.conf.example`
|
|
- `.containerignore`
|
|
|
|
### Configuration
|
|
- Update `.env.example` with feed/container variables
|
|
- No breaking changes to existing configuration
|
|
- Backward compatible
|
|
|
|
### Database
|
|
- No schema changes required
|
|
- No migrations needed
|
|
|
|
## Acceptance Criteria
|
|
|
|
Phase 5 is complete when:
|
|
|
|
### Functional Requirements
|
|
- [ ] RSS feed generates valid RSS 2.0 XML
|
|
- [ ] Feed includes recent published notes
|
|
- [ ] Feed respects configured item limit
|
|
- [ ] Feed has proper RFC-822 dates
|
|
- [ ] Feed includes HTML content in CDATA
|
|
- [ ] Feed route is accessible at /feed.xml
|
|
- [ ] Feed caching works (5 minutes)
|
|
- [ ] Feed discovery link in templates
|
|
- [ ] Container builds successfully
|
|
- [ ] Container runs application correctly
|
|
- [ ] Health check endpoint works
|
|
- [ ] Data persists across container restarts
|
|
- [ ] Container works with both Podman and Docker
|
|
- [ ] Compose configuration works
|
|
|
|
### Quality Requirements
|
|
- [ ] Feed validates with W3C validator
|
|
- [ ] Test coverage > 90%
|
|
- [ ] All tests pass
|
|
- [ ] No linting errors (flake8)
|
|
- [ ] Code formatted (black)
|
|
- [ ] Container image < 250MB
|
|
- [ ] Container startup < 10 seconds
|
|
- [ ] Memory usage < 512MB under load
|
|
|
|
### Security Requirements
|
|
- [ ] Feed only shows published notes
|
|
- [ ] Container runs as non-root user
|
|
- [ ] No secrets in container image
|
|
- [ ] Security headers configured
|
|
- [ ] HTTPS enforced in production config
|
|
- [ ] Resource limits configured
|
|
|
|
### Documentation Requirements
|
|
- [ ] RSS implementation documented
|
|
- [ ] Container build documented
|
|
- [ ] Deployment guide complete
|
|
- [ ] Reverse proxy configs provided
|
|
- [ ] IndieAuth testing documented
|
|
- [ ] CHANGELOG updated
|
|
- [ ] Version incremented to 0.6.0
|
|
|
|
### Production Testing Requirements
|
|
- [ ] Container deployed with public URL
|
|
- [ ] IndieAuth tested with real IndieLogin
|
|
- [ ] RSS feed accessible via HTTPS
|
|
- [ ] Feed appears in RSS readers
|
|
- [ ] Session persistence works
|
|
- [ ] Container restarts gracefully
|
|
|
|
## Implementation Checklist
|
|
|
|
### Phase 5.1: RSS Feed Implementation
|
|
- [ ] Create `starpunk/feed.py` module
|
|
- [ ] Implement `generate_feed()` function
|
|
- [ ] Implement `format_rfc822_date()` function
|
|
- [ ] Implement `get_note_title()` function
|
|
- [ ] Implement `clean_html_for_rss()` function
|
|
- [ ] Add feed route to `public.py`
|
|
- [ ] Implement feed caching
|
|
- [ ] Add ETag support
|
|
- [ ] Update templates with RSS discovery
|
|
- [ ] Test feed generation
|
|
|
|
### Phase 5.2: Feed Testing
|
|
- [ ] Write unit tests for feed.py
|
|
- [ ] Write route tests for feed endpoint
|
|
- [ ] Test caching behavior
|
|
- [ ] Test ETag validation
|
|
- [ ] Validate with W3C Feed Validator
|
|
- [ ] Test with RSS readers
|
|
- [ ] Verify date formatting
|
|
- [ ] Check HTML rendering
|
|
|
|
### Phase 5.3: Container Implementation
|
|
- [ ] Create Containerfile
|
|
- [ ] Create compose.yaml
|
|
- [ ] Create .containerignore
|
|
- [ ] Add health check endpoint
|
|
- [ ] Configure Gunicorn
|
|
- [ ] Test container build
|
|
- [ ] Test container run
|
|
- [ ] Test data persistence
|
|
- [ ] Test health checks
|
|
|
|
### Phase 5.4: Production Configuration
|
|
- [ ] Create Caddyfile.example
|
|
- [ ] Create nginx.conf.example
|
|
- [ ] Update .env.example
|
|
- [ ] Document environment variables
|
|
- [ ] Create deployment guide
|
|
- [ ] Test reverse proxy configs
|
|
- [ ] Document SSL setup
|
|
- [ ] Document IndieAuth testing
|
|
|
|
### Phase 5.5: Integration Testing
|
|
- [ ] Build and run container
|
|
- [ ] Test with Podman
|
|
- [ ] Test with Docker
|
|
- [ ] Test compose orchestration
|
|
- [ ] Test health endpoint
|
|
- [ ] Test feed accessibility
|
|
- [ ] Test data persistence
|
|
- [ ] Test graceful shutdown
|
|
|
|
### Phase 5.6: Production Deployment Testing
|
|
- [ ] Deploy to public-facing server
|
|
- [ ] Configure reverse proxy with HTTPS
|
|
- [ ] Test IndieAuth authentication
|
|
- [ ] Verify callback URLs work
|
|
- [ ] Test session creation
|
|
- [ ] Verify feed in RSS reader
|
|
- [ ] Check security headers
|
|
- [ ] Monitor resource usage
|
|
|
|
### Phase 5.7: Documentation
|
|
- [ ] Complete ADR-014 (RSS implementation)
|
|
- [ ] Document feed generation
|
|
- [ ] Document container setup
|
|
- [ ] Document deployment process
|
|
- [ ] Document IndieAuth testing
|
|
- [ ] Update CHANGELOG
|
|
- [ ] Increment version to 0.6.0
|
|
- [ ] Create quick reference guide
|
|
|
|
## Success Metrics
|
|
|
|
Phase 5 is successful if:
|
|
|
|
1. **RSS feed is valid** and passes W3C validation
|
|
2. **Feed appears in RSS readers** correctly
|
|
3. **Container builds** in under 2 minutes
|
|
4. **Container runs** reliably with no crashes
|
|
5. **IndieAuth works** with public HTTPS URL
|
|
6. **Data persists** across container restarts
|
|
7. **Memory usage** stays under 512MB
|
|
8. **Feed caching** reduces load
|
|
9. **All tests pass** with >90% coverage
|
|
10. **Documentation** enables easy deployment
|
|
|
|
## Dependencies
|
|
|
|
### Python Packages (Existing)
|
|
- feedgen (already in requirements.txt)
|
|
- Flask
|
|
- markdown
|
|
- httpx
|
|
|
|
### No New Python Dependencies Required
|
|
|
|
### External Dependencies
|
|
- Podman or Docker (user provides)
|
|
- Reverse proxy: Caddy or Nginx (user provides)
|
|
- Public domain with HTTPS (user provides)
|
|
|
|
### Build Tools
|
|
- Container runtime (podman/docker)
|
|
- Python 3.11+
|
|
- uv (for fast dependency installation)
|
|
|
|
## Risk Assessment
|
|
|
|
### Technical Risks
|
|
|
|
**Risk: Feed caching causes stale content**
|
|
- Likelihood: Low
|
|
- Impact: Low
|
|
- Mitigation: 5-minute cache is acceptable delay, ETag support, clear cache on note creation
|
|
|
|
**Risk: Container fails to start**
|
|
- Likelihood: Low
|
|
- Impact: High
|
|
- Mitigation: Health checks, startup probes, extensive testing, fallback to direct deployment
|
|
|
|
**Risk: IndieAuth callback fails with HTTPS**
|
|
- Likelihood: Medium
|
|
- Impact: High
|
|
- Mitigation: Clear documentation, example configs, testing guide, troubleshooting section
|
|
|
|
**Risk: Data loss on container restart**
|
|
- Likelihood: Low
|
|
- Impact: Critical
|
|
- Mitigation: Volume mounts, data persistence testing, backup documentation
|
|
|
|
### Operational Risks
|
|
|
|
**Risk: Incorrect reverse proxy configuration**
|
|
- Likelihood: Medium
|
|
- Impact: High
|
|
- Mitigation: Example configs, testing guide, common issues documentation
|
|
|
|
**Risk: Resource exhaustion in container**
|
|
- Likelihood: Low
|
|
- Impact: Medium
|
|
- Mitigation: Resource limits, health checks, monitoring guidance
|
|
|
|
**Risk: RSS feed validation failures**
|
|
- Likelihood: Low
|
|
- Impact: Low
|
|
- Mitigation: Extensive testing, W3C validation, feed reader testing
|
|
|
|
## Future Enhancements (V2+)
|
|
|
|
Deferred features:
|
|
- Feed pagination (older entries)
|
|
- Multiple feed formats (Atom, JSON Feed)
|
|
- Feed categories/tags
|
|
- Feed discovery for individual tags
|
|
- Feed analytics
|
|
- Podcast RSS support
|
|
- Media enclosures
|
|
- Container orchestration (Kubernetes)
|
|
- Container registry publishing
|
|
- Auto-update mechanisms
|
|
- Feed notification (WebSub)
|
|
|
|
## References
|
|
|
|
### Internal Documentation
|
|
- [Phase 4: Web Interface](/home/phil/Projects/starpunk/docs/design/phase-4-web-interface.md)
|
|
- [Architecture Overview](/home/phil/Projects/starpunk/docs/architecture/overview.md)
|
|
- [ADR-002: Flask Extensions](/home/phil/Projects/starpunk/docs/decisions/ADR-002-flask-extensions.md)
|
|
- [Versioning Strategy](/home/phil/Projects/starpunk/docs/standards/versioning-strategy.md)
|
|
|
|
### External Standards
|
|
- [RSS 2.0 Specification](https://www.rssboard.org/rss-specification)
|
|
- [RFC-822 Date Format](https://www.rfc-editor.org/rfc/rfc822)
|
|
- [W3C Feed Validator](https://validator.w3.org/feed/)
|
|
- [Dockerfile Best Practices](https://docs.docker.com/develop/dev-best-practices/)
|
|
- [Podman Documentation](https://docs.podman.io/)
|
|
- [Gunicorn Configuration](https://docs.gunicorn.org/en/stable/settings.html)
|
|
|
|
---
|
|
|
|
**Phase**: 5
|
|
**Version Target**: 0.6.0
|
|
**Status**: Design Complete, Ready for Implementation
|
|
**Next Phase**: Phase 6 (Micropub Implementation)
|