The code that runs my weblog, https://taylorlearns.com/
This blog is built on a Django-based foundation originally developed by Simon Willison. The following documentation includes insights from Simon's development and deployment practices, particularly from his posts in 2023-2024.
- Django: High-level Python web framework
- PostgreSQL: Database with full-text search capabilities
- Gunicorn: Python WSGI HTTP Server for production
- WhiteNoise: Static file serving
- Django Admin: Content management interface
The blog uses custom Django models that inherit from a BaseModel for shared functionality:
- Entry: Full blog posts
- Blogmark: Link posts (as described in Simon's December 2024 post)
- Quotation: Quoted excerpts
- Note: Short notes
All models share features like:
- Tagging system
- Draft mode for previewing unpublished content
- Full-text search integration
Draft mode allows you to assign URLs to items for previewing in the browser without publishing them. This is particularly useful for reviewing content before it goes live. As Simon mentioned in his December 2024 post on link blogging, this feature helps streamline the content creation workflow.
Based on Simon's practices:
- Images are converted to optimized JPEGs
- Uploaded to S3 bucket for storage
- Served via Cloudflare's free tier for cost-effective CDN delivery
- Alt text generation using language models (optional enhancement)
This blog includes a built-in faceted search engine using Django and PostgreSQL:
- The search functionality is implemented in the
searchfunction inblog/search.py. - It uses a combination of full-text search and tag-based filtering.
- The search index is built and updated automatically when new content is added to the blog.
- Users can search for content using keywords, which are matched against the full text of blog entries and blogmarks.
- The search results are ranked based on relevance and can be further filtered by tags.
- The search interface is integrated into the blog's user interface, allowing for a seamless user experience.
For more details on the implementation, refer to the search function in blog/search.py.
The blog is automatically deployed to a Hetzner VPS using GitHub Actions:
- CI/CD Pipeline:
.github/workflows/ci.ymlruns tests on every push/PR - Automated Deployment:
.github/workflows/deploy.ymldeploys to production on push tomain - Infrastructure: Docker Compose orchestrates Django and PostgreSQL containers
- Reverse Proxy: Nginx Proxy Manager handles SSL/TLS and proxies requests to Django
Hosting Environment:
- VPS Provider: Hetzner Cloud
- Operating System: Linux (Docker host)
- Orchestration: Docker Compose
- Reverse Proxy: Nginx Proxy Manager (running in Docker)
- Networking: Docker network (
app-stack_app-network) shared with other services
Application Stack:
- Django/Gunicorn: Application server (4 workers, 2 threads each)
- PostgreSQL 15: Database with persistent Docker volume
- WhiteNoise: Static file serving with content hashing
- Docker: Containerization for consistent deployments
How Automatic Deployment Works:
- Developer pushes code to
mainbranch - GitHub Actions CI runs tests automatically
- If tests pass, deployment workflow triggers:
- Connects to VPS via SSH (using GitHub Secrets)
- Pulls latest code from GitHub repository
- Builds new Docker image with updated code
- Recreates Django container (zero-downtime)
- Runs database migrations
- Verifies deployment with health check
Required GitHub Secrets:
SSH_PRIVATE_KEY: SSH key for server accessVPS_HOST: Server IP addressVPS_USER: Server usernameVPS_APP_DIR: Path to app-stack directory
See deployment/README.md for detailed setup instructions.
Achieved through Docker Compose's container recreation strategy:
- New Django container starts before old one stops
- Nginx Proxy Manager seamlessly switches traffic
- Database remains running (persistent volume)
- Health checks verify the new container before completion
- Static files versioned using content hashes (WhiteNoise's
CompressedManifestStaticFilesStorage) - Built into Docker image during build (not at runtime)
- Efficiently cached while ensuring fresh deployments reference correct versions
- No separate CDN required - WhiteNoise serves static files efficiently
GitHub Actions CI (.github/workflows/ci.yml) runs on every push/PR:
- Sets up PostgreSQL service container for testing
- Uses
uvfor fast dependency installation - Runs database migrations in test environment
- Collects static files to catch configuration errors
- Executes full test suite:
uv run python manage.py test -v3 - Provides immediate feedback on code quality
This project uses uv for package management and virtual environment handling. Install uv with:
curl -LsSf https://astral.sh/uv/install.sh | shOr on macOS with Homebrew:
brew install uv- Create and sync the virtual environment with uv:
uv syncThis will:
- Create a virtual environment (if it doesn't exist)
- Install all dependencies from
pyproject.toml - Install the project in editable mode
- Activate the virtual environment (uv will show you the path, or use):
source .venv/bin/activate # On macOS/Linux
# or
source .venv/Scripts/activate # On WindowsAlternatively, you can run commands directly with uv run:
uv run python manage.py migrate
uv run python manage.py runserverTo add a new dependency:
uv add package-nameTo add a development dependency:
uv add --dev package-nameTo update all dependencies:
uv sync --upgradeTo update a specific package:
uv add package-name@latest- Set the
DATABASE_URLenvironment variable topostgres://testuser:testpass@localhost/taylorlearnsblog. - Run tests from the repository root with:
uv run python manage.py test -v3Or if the virtual environment is activated:
python manage.py test -v3Key environment variables for configuration:
DATABASE_URL: PostgreSQL connection stringDJANGO_SECRET: Secret key for productionDJANGO_DEBUG: Enable debug modeSENTRY_DSN: Error monitoring (optional)- Cloudflare configuration:
CLOUDFLARE_EMAIL,CLOUDFLARE_TOKEN,CLOUDFLARE_ZONE_ID
Simon emphasizes comprehensive documentation:
- Treat projects as if you might forget every detail
- Use GitHub issues to document design decisions and project discussions
- Write thorough release notes for project updates
- This README serves as ongoing documentation of the project's evolution
For more insights into Simon Willison's blog development practices, see: