diff --git a/README.md b/README.md index 1f7a872..f5f1255 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ A Flask-based webcomic website with server-side rendering using Jinja2 templates - [Option 1: Docker (Recommended)](#option-1-docker-recommended) - [Option 2: Manual Deployment with Gunicorn](#option-2-manual-deployment-with-gunicorn) - [Using a Reverse Proxy (Recommended)](#using-a-reverse-proxy-recommended) + - [Using a CDN for Static Assets](#using-a-cdn-for-static-assets) - [Additional Production Considerations](#additional-production-considerations) - [Upgrading to a Database](#upgrading-to-a-database) - [Customization](#customization) @@ -172,6 +173,7 @@ Don't have a server? No problem! Here are beginner-friendly options to get your - JSON API for programmatic access - Open Graph and Twitter Card metadata for social sharing - Server-side rendering with Jinja2 +- **Built-in CDN support** for static assets (images, CSS, JavaScript) - **Comprehensive accessibility features** (WCAG compliant) - **Search engine optimized** (sitemap, robots.txt, meta tags, canonical URLs) @@ -724,6 +726,7 @@ The `comics_data.py` file contains global configuration options for your comic s COMIC_NAME = 'Sunday Comics' # Your comic/website name COPYRIGHT_NAME = None # Name for copyright (defaults to COMIC_NAME) SITE_URL = 'http://localhost:3000' # Your domain (update for production) +CDN_URL = None # CDN URL for static assets (None = use local) 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/) @@ -1035,12 +1038,67 @@ Set up Nginx or another reverse proxy in front of your app for: - Load balancing - Better security +### Using a CDN for Static Assets + +For better performance, especially with high traffic or global audiences, you can serve static assets (images, CSS, JavaScript) from a Content Delivery Network (CDN). + +**How it works:** + +Sunday Comics includes built-in CDN support through the `CDN_URL` configuration option. When set, all static assets (comic images, CSS, JavaScript, icons, etc.) are served from your CDN instead of your application server. + +**Setup Steps:** + +1. **Upload your static files to a CDN:** + - Upload the entire `static/` directory to your CDN provider + - Popular CDN options: Cloudflare, AWS CloudFront, BunnyCDN, KeyCDN + - Maintain the same directory structure (e.g., `static/css/style.css` → `your-cdn.com/static/css/style.css`) + +2. **Configure Sunday Comics:** + ```python + # In comics_data.py + CDN_URL = 'https://cdn.example.com' # No trailing slash! + ``` + +3. **Deploy and test:** + - Restart your application + - Verify images and CSS load from the CDN by checking network requests in browser DevTools + +**Benefits:** +- ⚡ **Faster loading** - Static assets served from edge servers closer to your readers +- 📉 **Reduced server load** - Your application server only handles dynamic content +- 💰 **Lower bandwidth costs** - CDN bandwidth is often cheaper than server bandwidth +- 🌍 **Global performance** - Readers worldwide get fast load times +- 🛡️ **DDoS protection** - Many CDNs include built-in protection + +**Example Configuration:** + +```python +# Local development (no CDN) +CDN_URL = None + +# Production with CDN +CDN_URL = 'https://d1abc123xyz.cloudfront.net' # AWS CloudFront example +CDN_URL = 'https://cdn.yourcomic.com' # Custom domain example +``` + +**Important Notes:** +- When using a CDN, update your static files on the CDN whenever you make changes +- Consider using cache-busting techniques for CSS/JS updates (version query parameters) +- Test thoroughly after enabling CDN to ensure all assets load correctly +- The `cdn_static` template filter automatically handles URL generation +- When `CDN_URL` is `None`, Sunday Comics falls back to local static file serving + +**Free CDN Options:** +- **Cloudflare** - Free tier includes CDN and DDoS protection +- **jsDelivr** - Free CDN for GitHub repositories +- **BunnyCDN** - Low-cost pay-as-you-go pricing + ### 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 +- Use a CDN for static assets (see [Using a CDN](#using-a-cdn-for-static-assets) above) - Monitor logs and performance - Set up automated backups of `comics_data.py` diff --git a/app.py b/app.py index 9fbc728..cd13116 100644 --- a/app.py +++ b/app.py @@ -7,7 +7,7 @@ import logging from datetime import datetime from flask import Flask, render_template, abort, jsonify, request from comics_data import ( - COMICS, COMIC_NAME, COPYRIGHT_NAME, SITE_URL, FULL_WIDTH_DEFAULT, PLAIN_DEFAULT, LOGO_IMAGE, LOGO_MODE, + COMICS, COMIC_NAME, COPYRIGHT_NAME, SITE_URL, CDN_URL, FULL_WIDTH_DEFAULT, PLAIN_DEFAULT, LOGO_IMAGE, LOGO_MODE, HEADER_IMAGE, FOOTER_IMAGE, BANNER_IMAGE, COMPACT_FOOTER, ARCHIVE_FULL_WIDTH, SECTIONS_ENABLED, USE_COMIC_NAV_ICONS, USE_HEADER_NAV_ICONS, USE_FOOTER_SOCIAL_ICONS, USE_SHARE_ICONS, NEWSLETTER_ENABLED, SOCIAL_INSTAGRAM, SOCIAL_YOUTUBE, SOCIAL_EMAIL, API_SPEC_LINK, EMBED_ENABLED, PERMALINK_ENABLED @@ -32,6 +32,24 @@ def add_ai_blocking_headers(response): return response +@app.template_filter('cdn_static') +def cdn_static(filename): + """Generate URL for static assets with CDN support + + When CDN_URL is set, returns CDN URL. Otherwise returns local static URL. + + Args: + filename: Path to static file (e.g., 'css/style.css') + + Returns: + Full URL to the static asset + """ + from flask import url_for + if CDN_URL: + return f"{CDN_URL}/static/{filename}" + return url_for('static', filename=filename) + + @app.context_processor def inject_global_settings(): """Make global settings available to all templates""" @@ -40,6 +58,7 @@ def inject_global_settings(): 'copyright_name': COPYRIGHT_NAME if COPYRIGHT_NAME else COMIC_NAME, 'current_year': datetime.now().year, 'site_url': SITE_URL, + 'cdn_url': CDN_URL, 'logo_image': LOGO_IMAGE, 'logo_mode': LOGO_MODE, 'header_image': HEADER_IMAGE, diff --git a/comics_data.py b/comics_data.py index 023e733..b93a3f6 100644 --- a/comics_data.py +++ b/comics_data.py @@ -15,6 +15,12 @@ COPYRIGHT_NAME = None # e.g., 'Your Name' or 'Your Studio Name' # Update this to your production domain when deploying SITE_URL = 'http://localhost:3000' +# Global setting: CDN URL for static assets (set to None to use local assets) +# When set, all static assets will be served from this CDN +# Example: CDN_URL = 'https://cdn.example.com' (no trailing slash) +# Leave as None for local development or if not using a CDN +CDN_URL = None + # Global setting: Set to True to make all comics full-width by default # Individual comics can override this with 'full_width': False FULL_WIDTH_DEFAULT = False diff --git a/templates/archive.html b/templates/archive.html index 4a344fd..147a37d 100644 --- a/templates/archive.html +++ b/templates/archive.html @@ -24,8 +24,8 @@ {% for comic in section_comics %}
- {{ comic.title if comic.title else '#' ~ comic.number }}
@@ -47,5 +47,5 @@ {% endblock %} {% block extra_js %} - + {% endblock %} diff --git a/templates/base.html b/templates/base.html index b352ae3..ff653d2 100644 --- a/templates/base.html +++ b/templates/base.html @@ -31,13 +31,13 @@ - - - - + + + + - - + + {% block extra_css %}{% endblock %} @@ -46,7 +46,7 @@ {% if header_image %}
- {{ comic_name }} Header + {{ comic_name }} Header
{% endif %} @@ -57,10 +57,10 @@
@@ -157,11 +157,11 @@

Share A Link

- +
@@ -175,7 +175,7 @@
- Sunday Comics + Sunday Comics
@@ -184,7 +184,7 @@ {% if footer_image %} {% endif %} @@ -206,12 +206,12 @@ {% endif %} - + {% if embed_enabled %} - + {% endif %} {% if permalink_enabled %} - + {% endif %} {% block extra_js %}{% endblock %} diff --git a/templates/comic.html b/templates/comic.html index ea7f72a..9bfd1be 100644 --- a/templates/comic.html +++ b/templates/comic.html @@ -2,7 +2,7 @@ {% block meta_description %}{{ comic.alt_text }}{% if comic.author_note %} - {{ comic.author_note }}{% endif %}{% endblock %} -{% block og_image %}{{ site_url }}/static/images/thumbs/{{ comic.filename }}{% endblock %} +{% block og_image %}{% if cdn_url %}{{ cdn_url }}/static/images/thumbs/{{ comic.filename }}{% else %}{{ site_url }}/static/images/thumbs/{{ comic.filename }}{% endif %}{% endblock %} {% block extra_css %}