Sunday Comics - Webcomic Flask App
A Flask-based webcomic website with server-side rendering using Jinja2 templates.
Features
- Comic viewer with navigation (First, Previous, Next, Latest)
- Client-side navigation using JSON API (no page reloads)
- Archive page with thumbnail grid
- RSS feed support
- Markdown support for author notes and about page
- Optional icon-based navigation (comic navigation, header, and social links)
- Configurable logo and header/footer images
- Mobile-responsive with optional mobile-specific comic images
- Full-width and plain (headerless) display modes
- JSON API for programmatic access
- Open Graph and Twitter Card metadata for social sharing
- Server-side rendering with Jinja2
Project Structure
sunday/
├── app.py # Main Flask application
├── comics_data.py # Comic data and configuration
├── requirements.txt # Python dependencies
├── Dockerfile # Production Docker image
├── docker-compose.yml # Docker Compose configuration
├── .dockerignore # Docker build exclusions
├── scripts/ # Utility scripts
│ ├── add_comic.py # Script to add new comic entries
│ └── generate_rss.py # Script to generate RSS feed
├── content/ # Markdown content
│ ├── about.md # About page content
│ └── author_notes/ # Author notes for comics (by date)
├── templates/ # Jinja2 templates
│ ├── base.html # Base template with navigation
│ ├── index.html # Latest comic page
│ ├── comic.html # Individual comic viewer
│ ├── archive.html # Archive grid
│ ├── page.html # Generic markdown page template
│ └── 404.html # Error page
└── static/ # Static files
├── css/
│ └── style.css # Main stylesheet
├── js/
│ └── comic-nav.js # Client-side navigation
├── images/ # Image directory
│ ├── comics/ # Comic images
│ ├── thumbs/ # Thumbnail images for archive
│ └── icons/ # Navigation and social icons (optional)
└── feed.rss # RSS feed (generated)
Setup
- Install dependencies:
pip install -r requirements.txt
- Run the application:
python app.py
- Visit http://127.0.0.1:3000 in your browser
Environment Variables
The app can be configured via environment variables:
SECRET_KEY- Flask secret key (defaults to 'your-secret-key')PORT- Port to run on (defaults to 3000)DEBUG- Enable debug mode (defaults to False)
Development:
export DEBUG=True
python app.py
Production:
export SECRET_KEY="your-secure-random-secret-key"
export PORT=3000
python app.py
Configuration
The comics_data.py file contains both comic data and global configuration options:
Global Settings
COMIC_NAME = 'Sunday Comics' # Your comic/website name
SITE_URL = 'http://localhost:3000' # Your domain (update for production)
FULL_WIDTH_DEFAULT = False # Make all comics full-width by default
PLAIN_DEFAULT = False # Hide header/remove borders by default
LOGO_IMAGE = 'logo.png' # Path to logo (relative to static/images/)
LOGO_MODE = 'beside' # 'beside' or 'replace' title with logo
HEADER_IMAGE = None # Optional header image path
FOOTER_IMAGE = None # Optional footer image path
COMPACT_FOOTER = False # Display footer in compact mode
ARCHIVE_FULL_WIDTH = True # Full-width archive with 4 columns
USE_COMIC_NAV_ICONS = True # Use icons for comic navigation buttons
USE_HEADER_NAV_ICONS = True # Show icons in main header navigation
USE_FOOTER_SOCIAL_ICONS = True # Use icons for social links
SOCIAL_INSTAGRAM = None # Instagram URL (or None)
SOCIAL_YOUTUBE = None # YouTube URL (or None)
SOCIAL_EMAIL = None # Email mailto link (or None)
API_SPEC_LINK = None # API documentation link (or None)
Adding Comics
Comics are stored in the COMICS list in comics_data.py. Each comic entry:
{
'number': 1, # Comic number (required, sequential)
'filename': 'comic-001.png', # Image filename (required)
'mobile_filename': 'comic-001-mobile.png', # Optional mobile version
'date': '2025-01-01', # Publication date (required)
'alt_text': 'Alt text for comic', # Accessibility text (required)
'title': 'Comic Title', # Title (optional, shows #X if absent)
'author_note': 'Optional note', # Author note (optional, plain text)
'full_width': True, # Optional: override FULL_WIDTH_DEFAULT
'plain': True # Optional: override PLAIN_DEFAULT
}
Adding a New Comic
Option 1: Use the script (recommended)
# Add comic entry only
python scripts/add_comic.py
# Add comic entry AND create markdown file for author notes
python scripts/add_comic.py -m
This will automatically add a new entry with defaults. The -m flag creates a markdown file in content/author_notes/{date}.md with a template. Then edit comics_data.py to customize.
Option 2: Manual
- Save your comic image in
static/images/comics/(e.g.,comic-001.png) - Optionally, create a thumbnail in
static/images/thumbs/with the same filename - Add the comic entry to the
COMICSlist incomics_data.py
Generating RSS Feed
After adding comics, regenerate the RSS feed:
python scripts/generate_rss.py
This creates/updates static/feed.rss
Markdown Support
Author Notes:
Create markdown files in content/author_notes/{date}.md (e.g., 2025-01-01.md) for rich-formatted author notes. Markdown author notes take precedence over the author_note field in comics_data.py and render as HTML.
Example:
# Create author note for a specific comic
echo "# My First Comic\n\nThis is **bold** text!" > content/author_notes/2025-01-01.md
About Page:
The /about route renders content/about.md as HTML. Edit this file to customize your about page with markdown formatting.
Note: Client-side navigation displays author notes as plain text. Markdown author notes only render properly on initial page load (server-side rendering). For full markdown rendering, users need to refresh the page or navigate directly to the comic URL.
Production Deployment
For production, you should NOT use Flask's built-in development server. Choose one of the following deployment methods:
Option 1: Docker (Recommended)
1. Generate a secure secret key:
python -c "import secrets; print(secrets.token_hex(32))"
2. Create a .env file:
SECRET_KEY=your-generated-secret-key-here
3. Build and run with Docker Compose:
docker-compose up -d
Or build and run manually:
# Build the image
docker build -t sunday-comics .
# Run the container
docker run -d \
-p 3000:3000 \
-e SECRET_KEY="your-secret-key" \
-v $(pwd)/comics_data.py:/app/comics_data.py:ro \
-v $(pwd)/static/images:/app/static/images:ro \
--name sunday-comics \
sunday-comics
View logs:
docker-compose logs -f
Option 2: Manual Deployment with Gunicorn
1. Generate a Secure Secret Key
python -c "import secrets; print(secrets.token_hex(32))"
2. Set Environment Variables
export SECRET_KEY="generated-secret-key-from-above"
export DEBUG=False
export PORT=3000
3. Use a Production WSGI Server
Install Gunicorn:
pip install gunicorn
Run with Gunicorn:
gunicorn app:app --bind 0.0.0.0:3000 --workers 4
Using a Reverse Proxy (Recommended)
Set up Nginx or another reverse proxy in front of your app for:
- HTTPS/SSL termination
- Static file serving
- Load balancing
- Better security
Additional Production Considerations
- Use a process manager (systemd, supervisor) for non-Docker deployments
- Set appropriate file permissions
- Enable HTTPS with Let's Encrypt
- Consider using a CDN for static assets
- Monitor logs and performance
- Set up automated backups of
comics_data.py
Upgrading to a Database
For larger comic archives, consider replacing the COMICS list with a database:
- SQLite for simple setups
- PostgreSQL/MySQL for production
- Use Flask-SQLAlchemy for ORM support
Customization
Branding
- Update
COMIC_NAMEincomics_data.pyto change your comic's name - Update
SITE_URLincomics_data.pyfor production deployment - Customize logo by setting
LOGO_IMAGEandLOGO_MODE
Styling
- Modify
static/css/style.cssto change colors, fonts, and layout - Current color scheme uses blue (#3498db) and dark blue-gray (#2c3e50)
About Page
- Edit
content/about.mdto add your bio and comic information (supports markdown)
Icon Navigation
To use icon navigation:
- Set
USE_COMIC_NAV_ICONS = Trueincomics_data.py - Add icons to
static/images/icons/:- Comic navigation:
first.png,previous.png,next.png,latest.png - Header navigation:
alert.png,archive.png,info.png - Social links:
instagram.png,youtube.png,mail.png
- Comic navigation:
Social Links
Configure social media links in comics_data.py:
SOCIAL_INSTAGRAM = 'https://instagram.com/yourhandle'
SOCIAL_YOUTUBE = 'https://youtube.com/@yourchannel'
SOCIAL_EMAIL = 'mailto:your@email.com'
Pages
/- Shows the latest comic/comic/<id>- View a specific comic by number/archive- Browse all comics in a grid/about- About the comic and author/static/feed.rss- RSS feed
API Endpoints
The app exposes a JSON API for programmatic access:
-
GET
/api/comics- Returns all comics as a JSON array[ { "number": 1, "title": "First Comic", "filename": "comic-001.png", "date": "2025-01-01", "alt_text": "The very first comic", "author_note": "This is where your comic journey begins!" } ] -
GET
/api/comics/<id>- Returns a specific comic as JSON{ "number": 1, "title": "First Comic", "filename": "comic-001.png", "date": "2025-01-01", "alt_text": "The very first comic", "author_note": "This is where your comic journey begins!" }Returns 404 with
{"error": "Comic not found"}if the comic doesn't exist.
Credits
- Inspired by Rarebit
- Favicon generated using favicon.io
- Example comics sourced from Boots and Her Buddies comic strip at Comic Book Plus
License
[Add your license here]