:lightning: CDN option

This commit is contained in:
mi
2025-11-15 21:24:44 +10:00
parent 6b3d446207
commit 0381908610
8 changed files with 154 additions and 71 deletions

View File

@@ -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`

21
app.py
View File

@@ -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,

View File

@@ -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

View File

@@ -24,8 +24,8 @@
{% for comic in section_comics %}
<div class="archive-item{% if archive_full_width %} archive-item-fullwidth{% endif %}">
<a href="{{ url_for('comic', comic_id=comic.number) }}">
<img src="{{ url_for('static', filename='images/thumbs/' + comic.filename) }}"
onerror="this.onerror=null; this.src='{{ url_for('static', filename='images/thumbs/default.jpg') }}';"
<img src="{{ ('images/thumbs/' + comic.filename) | cdn_static }}"
onerror="this.onerror=null; this.src='{{ 'images/thumbs/default.jpg' | cdn_static }}';"
alt="{{ comic.title if comic.title else '#' ~ comic.number }}"
loading="lazy">
<div class="archive-info">
@@ -47,5 +47,5 @@
{% endblock %}
{% block extra_js %}
<script src="{{ url_for('static', filename='js/archive-lazy-load.js') }}"></script>
<script src="{{ 'js/archive-lazy-load.js' | cdn_static }}"></script>
{% endblock %}

View File

@@ -31,13 +31,13 @@
<meta property="twitter:image" content="{% block twitter_image %}{{ self.og_image() }}{% endblock %}">
<!-- Favicon -->
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='favicon-32x32.png') }}">
<link rel="icon" type="image/png" sizes="16x16" href="{{ url_for('static', filename='favicon-16x16.png') }}">
<link rel="apple-touch-icon" sizes="180x180" href="{{ url_for('static', filename='apple-touch-icon.png') }}">
<link rel="icon" type="image/x-icon" href="{{ 'favicon.ico' | cdn_static }}">
<link rel="icon" type="image/png" sizes="32x32" href="{{ 'favicon-32x32.png' | cdn_static }}">
<link rel="icon" type="image/png" sizes="16x16" href="{{ 'favicon-16x16.png' | cdn_static }}">
<link rel="apple-touch-icon" sizes="180x180" href="{{ 'apple-touch-icon.png' | cdn_static }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<link rel="alternate" type="application/rss+xml" title="{{ comic_name }} RSS Feed" href="{{ url_for('static', filename='feed.rss') }}">
<link rel="stylesheet" href="{{ 'css/style.css' | cdn_static }}">
<link rel="alternate" type="application/rss+xml" title="{{ comic_name }} RSS Feed" href="{{ 'feed.rss' | cdn_static }}">
{% block extra_css %}{% endblock %}
</head>
<body data-site-url="{{ site_url }}">
@@ -46,7 +46,7 @@
{% if header_image %}
<div class="site-header-image">
<img src="{{ url_for('static', filename='images/' + header_image) }}" alt="{{ comic_name }} Header">
<img src="{{ ('images/' + header_image) | cdn_static }}" alt="{{ comic_name }} Header">
</div>
{% endif %}
@@ -57,10 +57,10 @@
<div class="nav-brand">
<a href="{{ url_for('index') }}">
{% if logo_image and logo_mode == 'beside' %}
<img src="{{ url_for('static', filename='images/' + logo_image) }}" alt="{{ comic_name }} Logo" class="nav-logo nav-logo-beside">
<img src="{{ ('images/' + logo_image) | cdn_static }}" alt="{{ comic_name }} Logo" class="nav-logo nav-logo-beside">
<span class="nav-title">{{ comic_name }}</span>
{% elif logo_image and logo_mode == 'replace' %}
<img src="{{ url_for('static', filename='images/' + logo_image) }}" alt="{{ comic_name }}" class="nav-logo nav-logo-replace">
<img src="{{ ('images/' + logo_image) | cdn_static }}" alt="{{ comic_name }}" class="nav-logo nav-logo-replace">
{% else %}
{{ comic_name }}
{% endif %}
@@ -70,17 +70,17 @@
<ul class="nav-links">
<li>
<a href="{{ url_for('index') }}" {% if request.endpoint == 'index' %}class="active"{% endif %}>
{% if use_header_nav_icons %}<img src="{{ url_for('static', filename='images/icons/alert.png') }}" alt="" class="nav-icon" aria-hidden="true">{% endif %}Latest
{% if use_header_nav_icons %}<img src="{{ 'images/icons/alert.png' | cdn_static }}" alt="" class="nav-icon" aria-hidden="true">{% endif %}Latest
</a>
</li>
<li>
<a href="{{ url_for('archive') }}" {% if request.endpoint == 'archive' %}class="active"{% endif %}>
{% if use_header_nav_icons %}<img src="{{ url_for('static', filename='images/icons/archive.png') }}" alt="" class="nav-icon" aria-hidden="true">{% endif %}Archive
{% if use_header_nav_icons %}<img src="{{ 'images/icons/archive.png' | cdn_static }}" alt="" class="nav-icon" aria-hidden="true">{% endif %}Archive
</a>
</li>
<li>
<a href="{{ url_for('about') }}" {% if request.endpoint == 'about' %}class="active"{% endif %}>
{% if use_header_nav_icons %}<img src="{{ url_for('static', filename='images/icons/info.png') }}" alt="" class="nav-icon" aria-hidden="true">{% endif %}About
{% if use_header_nav_icons %}<img src="{{ 'images/icons/info.png' | cdn_static }}" alt="" class="nav-icon" aria-hidden="true">{% endif %}About
</a>
</li>
</ul>
@@ -103,7 +103,7 @@
{% if social_instagram %}
<a href="{{ social_instagram }}" target="_blank" rel="noopener noreferrer" aria-label="Instagram">
{% if use_footer_social_icons %}
<img src="{{ url_for('static', filename='images/icons/instagram.png') }}" alt="" class="social-icon">
<img src="{{ 'images/icons/instagram.png' | cdn_static }}" alt="" class="social-icon">
{% else %}
Instagram
{% endif %}
@@ -112,7 +112,7 @@
{% if social_youtube %}
<a href="{{ social_youtube }}" target="_blank" rel="noopener noreferrer" aria-label="YouTube">
{% if use_footer_social_icons %}
<img src="{{ url_for('static', filename='images/icons/youtube.png') }}" alt="" class="social-icon">
<img src="{{ 'images/icons/youtube.png' | cdn_static }}" alt="" class="social-icon">
{% else %}
YouTube
{% endif %}
@@ -121,21 +121,21 @@
{% if social_email %}
<a href="{{ social_email }}" aria-label="Email">
{% if use_footer_social_icons %}
<img src="{{ url_for('static', filename='images/icons/mail .png') }}" alt="" class="social-icon">
<img src="{{ 'images/icons/mail .png' | cdn_static }}" alt="" class="social-icon">
{% else %}
Email
{% endif %}
</a>
{% endif %}
<a href="{{ url_for('static', filename='feed.rss') }}" aria-label="RSS Feed">
<a href="{{ 'feed.rss' | cdn_static }}" aria-label="RSS Feed">
{% if use_footer_social_icons %}
<img src="{{ url_for('static', filename='images/icons/rss.png') }}" alt="" class="social-icon">
<img src="{{ 'images/icons/rss.png' | cdn_static }}" alt="" class="social-icon">
{% else %}
RSS Feed
{% endif %}
</a>
{% if api_spec_link %}
<a href="{{ url_for('static', filename=api_spec_link) }}" aria-label="API">API</a>
<a href="{{ api_spec_link | cdn_static }}" aria-label="API">API</a>
{% endif %}
</div>
</div>
@@ -157,11 +157,11 @@
<h3>Share A Link</h3>
<div class="shareable-banner">
<a href="{{ site_url }}" target="_blank" rel="noopener noreferrer" aria-label="Link to {{ comic_name }} home page">
<img src="{{ url_for('static', filename='images/' + banner_image) }}" alt="{{ comic_name }}" class="banner-image">
<img src="{{ ('images/' + banner_image) | cdn_static }}" alt="{{ comic_name }}" class="banner-image">
</a>
<details class="banner-code">
<summary>Get code</summary>
<textarea readonly onfocus="this.select()" onclick="this.select()" aria-label="HTML code for linking to {{ comic_name }}"><a href="{{ site_url }}"><img src="{{ site_url }}/static/images/{{ banner_image }}" alt="{{ comic_name }}"></a></textarea>
<textarea readonly onfocus="this.select()" onclick="this.select()" aria-label="HTML code for linking to {{ comic_name }}"><a href="{{ site_url }}"><img src="{% if cdn_url %}{{ cdn_url }}/static/images/{{ banner_image }}{% else %}{{ site_url }}/static/images/{{ banner_image }}{% endif %}" alt="{{ comic_name }}"></a></textarea>
</details>
</div>
</div>
@@ -175,7 +175,7 @@
<span class="footer-divider" aria-hidden="true">|</span>
<div class="site-credit">
<a href="https://git.puercito.net/mi/sunday" target="_blank" rel="noopener noreferrer" aria-label="Sunday Comics - Webcomic platform">
<img src="{{ url_for('static', filename='images/sunday.jpg') }}" alt="Sunday Comics" class="credit-image">
<img src="{{ 'images/sunday.jpg' | cdn_static }}" alt="Sunday Comics" class="credit-image">
</a>
</div>
</div>
@@ -184,7 +184,7 @@
{% if footer_image %}
<div class="site-footer-image">
<img src="{{ url_for('static', filename='images/' + footer_image) }}" alt="{{ comic_name }} Footer">
<img src="{{ ('images/' + footer_image) | cdn_static }}" alt="{{ comic_name }} Footer">
</div>
{% endif %}
@@ -206,12 +206,12 @@
</div>
{% endif %}
<script src="{{ url_for('static', filename='js/comic-nav.js') }}"></script>
<script src="{{ 'js/comic-nav.js' | cdn_static }}"></script>
{% if embed_enabled %}
<script src="{{ url_for('static', filename='js/embed.js') }}"></script>
<script src="{{ 'js/embed.js' | cdn_static }}"></script>
{% endif %}
{% if permalink_enabled %}
<script src="{{ url_for('static', filename='js/permalink.js') }}"></script>
<script src="{{ 'js/permalink.js' | cdn_static }}"></script>
{% endif %}
{% block extra_js %}{% endblock %}
</body>

View File

@@ -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 %}
<script type="application/ld+json">
@@ -11,8 +11,8 @@
"@type": "ComicStory",
"name": "{{ comic.title if comic.title else '#' ~ comic.number }}",
"datePublished": "{{ comic.date }}",
"image": "{{ site_url }}/static/images/comics/{{ comic.filename }}",
"thumbnailUrl": "{{ site_url }}/static/images/thumbs/{{ comic.filename }}",
"image": "{% if cdn_url %}{{ cdn_url }}/static/images/comics/{{ comic.filename }}{% else %}{{ site_url }}/static/images/comics/{{ comic.filename }}{% endif %}",
"thumbnailUrl": "{% if cdn_url %}{{ cdn_url }}/static/images/thumbs/{{ comic.filename }}{% else %}{{ site_url }}/static/images/thumbs/{{ comic.filename }}{% endif %}",
"description": "{{ comic.alt_text }}",
"isPartOf": {
"@type": "ComicSeries",
@@ -41,8 +41,8 @@
{% if comic.is_multi_image %}
{# Multi-image layout (webtoon style) - no click-through on individual images #}
{% for i in range(comic.filenames|length) %}
<img src="{% if loop.first %}{{ url_for('static', filename='images/comics/' + comic.filenames[i]) }}{% endif %}"
{% if not loop.first %}data-src="{{ url_for('static', filename='images/comics/' + comic.filenames[i]) }}" class="lazy-load"{% endif %}
<img src="{% if loop.first %}{{ ('images/comics/' + comic.filenames[i]) | cdn_static }}{% endif %}"
{% if not loop.first %}data-src="{{ ('images/comics/' + comic.filenames[i]) | cdn_static }}" class="lazy-load"{% endif %}
alt="{{ comic.alt_texts[i] }}"
title="{{ comic.alt_texts[i] }}"
loading="{% if loop.first %}eager{% else %}lazy{% endif %}">
@@ -53,13 +53,13 @@
<a href="{{ url_for('comic', comic_id=comic.number + 1) }}" aria-label="Click to view next comic">
{% if comic.mobile_filename %}
<picture>
<source media="(max-width: 768px)" srcset="{{ url_for('static', filename='images/comics/' + comic.mobile_filename) }}">
<img src="{{ url_for('static', filename='images/comics/' + comic.filename) }}"
<source media="(max-width: 768px)" srcset="{{ ('images/comics/' + comic.mobile_filename) | cdn_static }}">
<img src="{{ ('images/comics/' + comic.filename) | cdn_static }}"
alt="{{ comic.title if comic.title else '#' ~ comic.number }}"
title="{{ comic.alt_text }}">
</picture>
{% else %}
<img src="{{ url_for('static', filename='images/comics/' + comic.filename) }}"
<img src="{{ ('images/comics/' + comic.filename) | cdn_static }}"
alt="{{ comic.title if comic.title else '#' ~ comic.number }}"
title="{{ comic.alt_text }}"
loading="eager">
@@ -68,13 +68,13 @@
{% else %}
{% if comic.mobile_filename %}
<picture>
<source media="(max-width: 768px)" srcset="{{ url_for('static', filename='images/comics/' + comic.mobile_filename) }}">
<img src="{{ url_for('static', filename='images/comics/' + comic.filename) }}"
<source media="(max-width: 768px)" srcset="{{ ('images/comics/' + comic.mobile_filename) | cdn_static }}">
<img src="{{ ('images/comics/' + comic.filename) | cdn_static }}"
alt="{{ comic.title if comic.title else '#' ~ comic.number }}"
title="{{ comic.alt_text }}">
</picture>
{% else %}
<img src="{{ url_for('static', filename='images/comics/' + comic.filename) }}"
<img src="{{ ('images/comics/' + comic.filename) | cdn_static }}"
alt="{{ comic.title if comic.title else '#' ~ comic.number }}"
title="{{ comic.alt_text }}"
loading="eager">
@@ -89,17 +89,17 @@
{# Icon-based navigation #}
{% if comic.number > 1 %}
<a href="{{ url_for('comic', comic_id=1) }}" class="btn-icon-nav" aria-label="First">
<img src="{{ url_for('static', filename='images/icons/first.png') }}" alt="">
<img src="{{ 'images/icons/first.png' | cdn_static }}" alt="">
</a>
<a href="{{ url_for('comic', comic_id=comic.number - 1) }}" class="btn-icon-nav" aria-label="Previous">
<img src="{{ url_for('static', filename='images/icons/previous.png') }}" alt="">
<img src="{{ 'images/icons/previous.png' | cdn_static }}" alt="">
</a>
{% else %}
<span class="btn-icon-nav btn-icon-disabled" aria-label="First" aria-disabled="true">
<img src="{{ url_for('static', filename='images/icons/first.png') }}" alt="">
<img src="{{ 'images/icons/first.png' | cdn_static }}" alt="">
</span>
<span class="btn-icon-nav btn-icon-disabled" aria-label="Previous" aria-disabled="true">
<img src="{{ url_for('static', filename='images/icons/previous.png') }}" alt="">
<img src="{{ 'images/icons/previous.png' | cdn_static }}" alt="">
</span>
{% endif %}
@@ -107,17 +107,17 @@
{% if comic.number < total_comics %}
<a href="{{ url_for('comic', comic_id=comic.number + 1) }}" class="btn-icon-nav" aria-label="Next">
<img src="{{ url_for('static', filename='images/icons/next.png') }}" alt="">
<img src="{{ 'images/icons/next.png' | cdn_static }}" alt="">
</a>
<a href="{{ url_for('comic', comic_id=total_comics) }}" class="btn-icon-nav" aria-label="Latest">
<img src="{{ url_for('static', filename='images/icons/latest.png') }}" alt="">
<img src="{{ 'images/icons/latest.png' | cdn_static }}" alt="">
</a>
{% else %}
<span class="btn-icon-nav btn-icon-disabled" aria-label="Next" aria-disabled="true">
<img src="{{ url_for('static', filename='images/icons/next.png') }}" alt="">
<img src="{{ 'images/icons/next.png' | cdn_static }}" alt="">
</span>
<span class="btn-icon-nav btn-icon-disabled" aria-label="Latest" aria-disabled="true">
<img src="{{ url_for('static', filename='images/icons/latest.png') }}" alt="">
<img src="{{ 'images/icons/latest.png' | cdn_static }}" alt="">
</span>
{% endif %}
{% else %}
@@ -147,12 +147,12 @@
<div class="comic-share-section">
{% if permalink_enabled %}
<button class="btn-permalink{% if use_share_icons %} btn-with-icon{% endif %}" id="permalink-button" data-comic-number="{{ comic.number }}" aria-label="Copy permalink to this comic">
{% if use_share_icons %}<img src="{{ url_for('static', filename='images/icons/link.png') }}" alt="" class="btn-icon" aria-hidden="true">{% endif %}Copy Permalink
{% if use_share_icons %}<img src="{{ 'images/icons/link.png' | cdn_static }}" alt="" class="btn-icon" aria-hidden="true">{% endif %}Copy Permalink
</button>
{% endif %}
{% if embed_enabled %}
<button class="btn-embed{% if use_share_icons %} btn-with-icon{% endif %}" id="embed-button" data-comic-number="{{ comic.number }}" aria-label="Get embed code for this comic">
{% if use_share_icons %}<img src="{{ url_for('static', filename='images/icons/embed.png') }}" alt="" class="btn-icon" aria-hidden="true">{% endif %}Share/Embed
{% if use_share_icons %}<img src="{{ 'images/icons/embed.png' | cdn_static }}" alt="" class="btn-icon" aria-hidden="true">{% endif %}Share/Embed
</button>
{% endif %}
</div>

View File

@@ -127,21 +127,21 @@
<p class="embed-date">{{ comic.formatted_date }}</p>
</div>
{% if logo_image %}
<img src="{{ site_url }}/static/images/{{ logo_image }}" alt="{{ comic_name }}" class="embed-logo">
<img src="{% if cdn_url %}{{ cdn_url }}/static/images/{{ logo_image }}{% else %}{{ site_url }}/static/images/{{ logo_image }}{% endif %}" alt="{{ comic_name }}" class="embed-logo">
{% endif %}
</div>
<div class="embed-image-wrapper">
<a href="{{ site_url }}/comic/{{ comic.number }}" class="embed-image-link" target="_blank" rel="noopener noreferrer" aria-label="View {{ comic.title if comic.title else 'comic #' ~ comic.number }} on {{ comic_name }}">
{% if comic.mobile_filename %}
<picture>
<source media="(max-width: 768px)" srcset="{{ url_for('static', filename='images/comics/' + comic.mobile_filename) }}">
<img src="{{ url_for('static', filename='images/comics/' + comic.filename) }}"
<source media="(max-width: 768px)" srcset="{{ ('images/comics/' + comic.mobile_filename) | cdn_static }}">
<img src="{{ ('images/comics/' + comic.filename) | cdn_static }}"
alt="{{ comic.title if comic.title else '#' ~ comic.number }}"
title="{{ comic.alt_text }}"
class="embed-image">
</picture>
{% else %}
<img src="{{ url_for('static', filename='images/comics/' + comic.filename) }}"
<img src="{{ ('images/comics/' + comic.filename) | cdn_static }}"
alt="{{ comic.title if comic.title else '#' ~ comic.number }}"
title="{{ comic.alt_text }}"
class="embed-image">

View File

@@ -3,7 +3,7 @@
{% if comic %}
{% 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 %}
{% endif %}
{% block content %}
@@ -20,8 +20,8 @@
{% if comic.is_multi_image %}
{# Multi-image layout (webtoon style) - no click-through on individual images #}
{% for i in range(comic.filenames|length) %}
<img src="{% if loop.first %}{{ url_for('static', filename='images/comics/' + comic.filenames[i]) }}{% endif %}"
{% if not loop.first %}data-src="{{ url_for('static', filename='images/comics/' + comic.filenames[i]) }}" class="lazy-load"{% endif %}
<img src="{% if loop.first %}{{ ('images/comics/' + comic.filenames[i]) | cdn_static }}{% endif %}"
{% if not loop.first %}data-src="{{ ('images/comics/' + comic.filenames[i]) | cdn_static }}" class="lazy-load"{% endif %}
alt="{{ comic.alt_texts[i] }}"
title="{{ comic.alt_texts[i] }}"
loading="{% if loop.first %}eager{% else %}lazy{% endif %}">
@@ -29,13 +29,13 @@
{% else %}
{% if comic.mobile_filename %}
<picture>
<source media="(max-width: 768px)" srcset="{{ url_for('static', filename='images/comics/' + comic.mobile_filename) }}">
<img src="{{ url_for('static', filename='images/comics/' + comic.filename) }}"
<source media="(max-width: 768px)" srcset="{{ ('images/comics/' + comic.mobile_filename) | cdn_static }}">
<img src="{{ ('images/comics/' + comic.filename) | cdn_static }}"
alt="{{ comic.title if comic.title else '#' ~ comic.number }}"
title="{{ comic.alt_text }}">
</picture>
{% else %}
<img src="{{ url_for('static', filename='images/comics/' + comic.filename) }}"
<img src="{{ ('images/comics/' + comic.filename) | cdn_static }}"
alt="{{ comic.title if comic.title else '#' ~ comic.number }}"
title="{{ comic.alt_text }}">
{% endif %}
@@ -48,17 +48,17 @@
{# Icon-based navigation #}
{% if comic.number > 1 %}
<a href="{{ url_for('comic', comic_id=1) }}" class="btn-icon-nav" aria-label="First">
<img src="{{ url_for('static', filename='images/icons/first.png') }}" alt="">
<img src="{{ 'images/icons/first.png' | cdn_static }}" alt="">
</a>
<a href="{{ url_for('comic', comic_id=comic.number - 1) }}" class="btn-icon-nav" aria-label="Previous">
<img src="{{ url_for('static', filename='images/icons/previous.png') }}" alt="">
<img src="{{ 'images/icons/previous.png' | cdn_static }}" alt="">
</a>
{% else %}
<span class="btn-icon-nav btn-icon-disabled" aria-label="First" aria-disabled="true">
<img src="{{ url_for('static', filename='images/icons/first.png') }}" alt="">
<img src="{{ 'images/icons/first.png' | cdn_static }}" alt="">
</span>
<span class="btn-icon-nav btn-icon-disabled" aria-label="Previous" aria-disabled="true">
<img src="{{ url_for('static', filename='images/icons/previous.png') }}" alt="">
<img src="{{ 'images/icons/previous.png' | cdn_static }}" alt="">
</span>
{% endif %}
@@ -66,17 +66,17 @@
{% if comic.number < total_comics %}
<a href="{{ url_for('comic', comic_id=comic.number + 1) }}" class="btn-icon-nav" aria-label="Next">
<img src="{{ url_for('static', filename='images/icons/next.png') }}" alt="">
<img src="{{ 'images/icons/next.png' | cdn_static }}" alt="">
</a>
<a href="{{ url_for('comic', comic_id=total_comics) }}" class="btn-icon-nav" aria-label="Latest">
<img src="{{ url_for('static', filename='images/icons/latest.png') }}" alt="">
<img src="{{ 'images/icons/latest.png' | cdn_static }}" alt="">
</a>
{% else %}
<span class="btn-icon-nav btn-icon-disabled" aria-label="Next" aria-disabled="true">
<img src="{{ url_for('static', filename='images/icons/next.png') }}" alt="">
<img src="{{ 'images/icons/next.png' | cdn_static }}" alt="">
</span>
<span class="btn-icon-nav btn-icon-disabled" aria-label="Latest" aria-disabled="true">
<img src="{{ url_for('static', filename='images/icons/latest.png') }}" alt="">
<img src="{{ 'images/icons/latest.png' | cdn_static }}" alt="">
</span>
{% endif %}
{% else %}
@@ -106,12 +106,12 @@
<div class="comic-share-section">
{% if permalink_enabled %}
<button class="btn-permalink{% if use_share_icons %} btn-with-icon{% endif %}" id="permalink-button" data-comic-number="{{ comic.number }}" aria-label="Copy permalink to this comic">
{% if use_share_icons %}<img src="{{ url_for('static', filename='images/icons/link.png') }}" alt="" class="btn-icon" aria-hidden="true">{% endif %}Copy Permalink
{% if use_share_icons %}<img src="{{ 'images/icons/link.png' | cdn_static }}" alt="" class="btn-icon" aria-hidden="true">{% endif %}Copy Permalink
</button>
{% endif %}
{% if embed_enabled %}
<button class="btn-embed{% if use_share_icons %} btn-with-icon{% endif %}" id="embed-button" data-comic-number="{{ comic.number }}" aria-label="Get embed code for this comic">
{% if use_share_icons %}<img src="{{ url_for('static', filename='images/icons/embed.png') }}" alt="" class="btn-icon" aria-hidden="true">{% endif %}Share/Embed
{% if use_share_icons %}<img src="{{ 'images/icons/embed.png' | cdn_static }}" alt="" class="btn-icon" aria-hidden="true">{% endif %}Share/Embed
</button>
{% endif %}
</div>