Compare commits

...

3 Commits

Author SHA1 Message Date
mi
2b8f30ef82 Release version 2025.11.18
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 13:44:41 +10:00
mi
882eed90f9 embed support 2025-11-18 13:40:52 +10:00
mi
d374df6b0b 🎨 configurable social links 2025-11-18 13:22:56 +10:00
12 changed files with 111 additions and 61 deletions

View File

@@ -14,6 +14,19 @@ and this project uses date-based versioning (YYYY.MM.DD).
### Fixed ### Fixed
### Security ### Security
## [2025.11.18] - 2025-11-18
### Added
- HTML embed support for comics (`html_embed` field in YAML allows custom HTML instead of images)
- Configurable social links in footer (`SOCIAL_LINKS` in `comics_data.py`)
- Customizable newsletter section (`NEWSLETTER_ENABLED` and `NEWSLETTER_HTML` in `comics_data.py`)
- CDN option for serving static assets
- Upstream update workflow (`UPSTREAM.md`) for fork-friendly development
- Automatic cache building, RSS feed, and sitemap generation in Docker container
### Changed
- Footer social links now fully customizable with any platform (Instagram, YouTube, Patreon, etc.)
## [2025.11.15] - 2025-11-15 ## [2025.11.15] - 2025-11-15
### Added ### Added
@@ -25,5 +38,6 @@ and this project uses date-based versioning (YYYY.MM.DD).
- This CHANGELOG.md file to track version history - This CHANGELOG.md file to track version history
- Version bump script (`scripts/bump_version.py`) to automate releases - Version bump script (`scripts/bump_version.py`) to automate releases
[Unreleased]: https://git.puercito.net/mi/sunday/compare/v2025.11.15...HEAD [Unreleased]: https://git.puercito.net/mi/sunday/compare/v2025.11.18...HEAD
[2025.11.18]: https://git.puercito.net/mi/sunday/compare/v2025.11.15...v2025.11.18
[2025.11.15]: https://git.puercito.net/mi/sunday/releases/tag/v2025.11.15 [2025.11.15]: https://git.puercito.net/mi/sunday/releases/tag/v2025.11.15

View File

@@ -214,7 +214,8 @@ Performance with caching (1000 comics):
Each comic YAML file contains: Each comic YAML file contains:
- `number` (required): Sequential comic number - `number` (required): Sequential comic number
- `filename` (required): Image filename in `static/images/comics/` OR list of filenames for multi-image comics (webtoon style) - `filename` (required unless using html_embed): Image filename in `static/images/comics/` OR list of filenames for multi-image comics (webtoon style)
- `html_embed` (optional): Custom HTML to embed instead of an image (e.g., video player, widget). Takes precedence over `filename`.
- `date` (required): Publication date in YYYY-MM-DD format - `date` (required): Publication date in YYYY-MM-DD format
- `alt_text` (required): Accessibility text OR list of alt texts (one per image for multi-image comics) - `alt_text` (required): Accessibility text OR list of alt texts (one per image for multi-image comics)
- `title` (optional): Comic title (defaults to "#X" if absent) - `title` (optional): Comic title (defaults to "#X" if absent)
@@ -224,6 +225,18 @@ Each comic YAML file contains:
- `plain` (optional): Override global PLAIN_DEFAULT setting (hides header/border) - `plain` (optional): Override global PLAIN_DEFAULT setting (hides header/border)
- `section` (optional): Section/chapter title (e.g., "Chapter 1: Origins"). Add to first comic of a new section. - `section` (optional): Section/chapter title (e.g., "Chapter 1: Origins"). Add to first comic of a new section.
**HTML embeds in YAML:**
```yaml
html_embed: '<iframe src="https://www.youtube.com/embed/dQw4w9WgXcQ" width="560" height="315" frameborder="0" allowfullscreen></iframe>'
alt_text: "Video description for accessibility"
```
- Use `html_embed` to display custom HTML content instead of an image
- Useful for embedding videos, interactive widgets, or special content
- When `html_embed` is present, it takes precedence over `filename` and `mobile_filename`
- The HTML is rendered as-is using the `| safe` filter in templates
- No click-through navigation on HTML embeds (use navigation buttons instead)
- Still provide `alt_text` for accessibility context
**Multi-image comics (webtoon style) in YAML:** **Multi-image comics (webtoon style) in YAML:**
```yaml ```yaml
filename: filename:
@@ -258,9 +271,7 @@ Global configuration in `comics_data.py`:
- `USE_FOOTER_SOCIAL_ICONS`: Set to True to use icons instead of text for footer social links (uses instagram.png, youtube.png, mail.png, alert.png) - `USE_FOOTER_SOCIAL_ICONS`: Set to True to use icons instead of text for footer social links (uses instagram.png, youtube.png, mail.png, alert.png)
- `NEWSLETTER_ENABLED`: Set to True to show newsletter section in footer - `NEWSLETTER_ENABLED`: Set to True to show newsletter section in footer
- `NEWSLETTER_HTML`: Custom HTML for newsletter form (user pastes their service's form code here) - `NEWSLETTER_HTML`: Custom HTML for newsletter form (user pastes their service's form code here)
- `SOCIAL_INSTAGRAM`: Instagram URL (set to None to hide) - `SOCIAL_LINKS`: List of dicts for social media links. Each dict has 'label', 'url', and optional 'icon' (filename in static/images/icons/). Users can add any platform (Instagram, YouTube, Bluesky, Patreon, etc.)
- `SOCIAL_YOUTUBE`: YouTube URL (set to None to hide)
- `SOCIAL_EMAIL`: Email mailto link (set to None to hide)
### Markdown Support ### Markdown Support
@@ -291,7 +302,8 @@ Global configuration in `comics_data.py`:
Provides SPA-like navigation without page reloads: Provides SPA-like navigation without page reloads:
- Fetches comics from `/api/comics/<id>` - Fetches comics from `/api/comics/<id>`
- Updates DOM with `displayComic(comic)` function - Updates DOM with `displayComic(comic)` function
- Handles navigation buttons and image click-through - Handles navigation buttons and image click-through (disabled for HTML embeds and multi-image comics)
- Renders HTML embeds, multi-image comics, and single-image comics dynamically
- Uses History API to maintain proper URLs and browser back/forward - Uses History API to maintain proper URLs and browser back/forward
- Shows/hides header based on plain mode - Shows/hides header based on plain mode
- Adjusts container for full_width mode - Adjusts container for full_width mode
@@ -320,9 +332,7 @@ Global context variables injected into all templates:
- `use_footer_social_icons`: Boolean for footer social link icons from `comics_data.py` - `use_footer_social_icons`: Boolean for footer social link icons from `comics_data.py`
- `newsletter_enabled`: Boolean to show/hide newsletter section from `comics_data.py` - `newsletter_enabled`: Boolean to show/hide newsletter section from `comics_data.py`
- `newsletter_html`: Custom HTML for newsletter form from `comics_data.py` (rendered with `| safe` filter) - `newsletter_html`: Custom HTML for newsletter form from `comics_data.py` (rendered with `| safe` filter)
- `social_instagram`: Instagram URL from `comics_data.py` - `social_links`: List of social media link dicts from `comics_data.py` (each with 'label', 'url', 'icon')
- `social_youtube`: YouTube URL from `comics_data.py`
- `social_email`: Email link from `comics_data.py`
## Static Assets ## Static Assets

View File

@@ -1 +1 @@
2025.11.15 2025.11.18

6
app.py
View File

@@ -11,7 +11,7 @@ from comics_data import (
HEADER_IMAGE, FOOTER_IMAGE, BANNER_IMAGE, COMPACT_FOOTER, ARCHIVE_FULL_WIDTH, SECTIONS_ENABLED, 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, USE_COMIC_NAV_ICONS, USE_HEADER_NAV_ICONS, USE_FOOTER_SOCIAL_ICONS, USE_SHARE_ICONS,
NEWSLETTER_ENABLED, NEWSLETTER_HTML, NEWSLETTER_ENABLED, NEWSLETTER_HTML,
SOCIAL_INSTAGRAM, SOCIAL_YOUTUBE, SOCIAL_EMAIL, API_SPEC_LINK, EMBED_ENABLED, PERMALINK_ENABLED SOCIAL_LINKS, API_SPEC_LINK, EMBED_ENABLED, PERMALINK_ENABLED
) )
import markdown import markdown
from version import __version__ from version import __version__
@@ -74,9 +74,7 @@ def inject_global_settings():
'use_share_icons': USE_SHARE_ICONS, 'use_share_icons': USE_SHARE_ICONS,
'newsletter_enabled': NEWSLETTER_ENABLED, 'newsletter_enabled': NEWSLETTER_ENABLED,
'newsletter_html': NEWSLETTER_HTML, 'newsletter_html': NEWSLETTER_HTML,
'social_instagram': SOCIAL_INSTAGRAM, 'social_links': SOCIAL_LINKS,
'social_youtube': SOCIAL_YOUTUBE,
'social_email': SOCIAL_EMAIL,
'api_spec_link': API_SPEC_LINK, 'api_spec_link': API_SPEC_LINK,
'embed_enabled': EMBED_ENABLED, 'embed_enabled': EMBED_ENABLED,
'permalink_enabled': PERMALINK_ENABLED, 'permalink_enabled': PERMALINK_ENABLED,

View File

@@ -97,10 +97,19 @@ NEWSLETTER_ENABLED = False
# ''' # '''
NEWSLETTER_HTML = '<p class="newsletter-placeholder">Newsletter coming soon!</p>' NEWSLETTER_HTML = '<p class="newsletter-placeholder">Newsletter coming soon!</p>'
# Social media links - set to None to hide the link # Social media links - add/remove/reorder as needed
SOCIAL_INSTAGRAM = None # e.g., 'https://instagram.com/yourhandle' # Each link should have 'label', 'url', and optionally 'icon' (filename in static/images/icons/)
SOCIAL_YOUTUBE = None # e.g., 'https://youtube.com/@yourchannel' # Set to empty list [] to show no social links
SOCIAL_EMAIL = None # e.g., 'mailto:your@email.com' SOCIAL_LINKS = [
# Example links (uncomment and customize):
# {'label': 'Instagram', 'url': 'https://instagram.com/yourhandle', 'icon': 'instagram.png'},
# {'label': 'YouTube', 'url': 'https://youtube.com/@yourchannel', 'icon': 'youtube.png'},
# {'label': 'Email', 'url': 'mailto:your@email.com', 'icon': 'mail.png'},
# {'label': 'Bluesky', 'url': 'https://bsky.app/profile/yourhandle', 'icon': 'bluesky.png'},
# {'label': 'Patreon', 'url': 'https://patreon.com/yourname', 'icon': 'patreon.png'},
# {'label': 'Ko-fi', 'url': 'https://ko-fi.com/yourname', 'icon': 'kofi.png'},
# {'label': 'Mastodon', 'url': 'https://mastodon.social/@yourhandle', 'icon': 'mastodon.png'},
]
# API documentation link - set to None to hide the link # API documentation link - set to None to hide the link
# Path is relative to static/ directory # Path is relative to static/ directory

View File

@@ -97,10 +97,19 @@ NEWSLETTER_ENABLED = False
# ''' # '''
NEWSLETTER_HTML = '<p class="newsletter-placeholder">Newsletter coming soon!</p>' NEWSLETTER_HTML = '<p class="newsletter-placeholder">Newsletter coming soon!</p>'
# Social media links - set to None to hide the link # Social media links - add/remove/reorder as needed
SOCIAL_INSTAGRAM = None # e.g., 'https://instagram.com/yourhandle' # Each link should have 'label', 'url', and optionally 'icon' (filename in static/images/icons/)
SOCIAL_YOUTUBE = None # e.g., 'https://youtube.com/@yourchannel' # Set to empty list [] to show no social links
SOCIAL_EMAIL = None # e.g., 'mailto:your@email.com' SOCIAL_LINKS = [
# Example links (uncomment and customize):
# {'label': 'Instagram', 'url': 'https://instagram.com/yourhandle', 'icon': 'instagram.png'},
# {'label': 'YouTube', 'url': 'https://youtube.com/@yourchannel', 'icon': 'youtube.png'},
# {'label': 'Email', 'url': 'mailto:your@email.com', 'icon': 'mail.png'},
# {'label': 'Bluesky', 'url': 'https://bsky.app/profile/yourhandle', 'icon': 'bluesky.png'},
# {'label': 'Patreon', 'url': 'https://patreon.com/yourname', 'icon': 'patreon.png'},
# {'label': 'Ko-fi', 'url': 'https://ko-fi.com/yourname', 'icon': 'kofi.png'},
# {'label': 'Mastodon', 'url': 'https://mastodon.social/@yourhandle', 'icon': 'mastodon.png'},
]
# API documentation link - set to None to hide the link # API documentation link - set to None to hide the link
# Path is relative to static/ directory # Path is relative to static/ directory

View File

@@ -6,7 +6,7 @@
# REQUIRED: Sequential comic number # REQUIRED: Sequential comic number
number: 999 number: 999
# REQUIRED: Image filename(s) in static/images/comics/ # REQUIRED (unless using html_embed): Image filename(s) in static/images/comics/
# Single image: # Single image:
filename: comic-999.jpg filename: comic-999.jpg
# OR multi-image (webtoon style): # OR multi-image (webtoon style):
@@ -18,6 +18,12 @@ filename: comic-999.jpg
# Optional: Mobile-optimized version of the comic # Optional: Mobile-optimized version of the comic
# mobile_filename: comic-999-mobile.jpg # mobile_filename: comic-999-mobile.jpg
# Optional: HTML embed instead of image
# Use this to embed videos, widgets, or other HTML content
# When set, this takes precedence over filename/mobile_filename
# Example: '<iframe src="https://www.youtube.com/embed/..." width="560" height="315"></iframe>'
# html_embed: '<div>Your custom HTML here</div>'
# REQUIRED: Publication date (YYYY-MM-DD format) # REQUIRED: Publication date (YYYY-MM-DD format)
date: "2025-01-01" date: "2025-01-01"

View File

@@ -80,8 +80,8 @@
const comicImageDiv = document.querySelector('.comic-image'); const comicImageDiv = document.querySelector('.comic-image');
updateComicImage(comicImageDiv, comic, title); updateComicImage(comicImageDiv, comic, title);
// Update or create/remove the link wrapper (only for single-image comics) // Update or create/remove the link wrapper (only for single-image comics, not HTML embeds)
if (!comic.is_multi_image) { if (!comic.is_multi_image && !comic.html_embed) {
updateComicImageLink(comic.number); updateComicImageLink(comic.number);
} }
@@ -181,15 +181,26 @@
// Clear all existing content // Clear all existing content
comicImageDiv.innerHTML = ''; comicImageDiv.innerHTML = '';
// Update container class for multi-image // Update container classes
if (comic.is_multi_image) { if (comic.html_embed) {
comicImageDiv.classList.add('comic-image-embed');
comicImageDiv.classList.remove('comic-image-multi');
} else if (comic.is_multi_image) {
comicImageDiv.classList.add('comic-image-multi'); comicImageDiv.classList.add('comic-image-multi');
comicImageDiv.classList.remove('comic-image-embed');
} else { } else {
comicImageDiv.classList.remove('comic-image-multi'); comicImageDiv.classList.remove('comic-image-multi');
comicImageDiv.classList.remove('comic-image-embed');
} }
// Create new image element(s) // Create new content
if (comic.is_multi_image) { if (comic.html_embed) {
// HTML embed (video, widget, etc.)
const embedWrapper = document.createElement('div');
embedWrapper.className = 'comic-embed-wrapper';
embedWrapper.innerHTML = comic.html_embed;
comicImageDiv.appendChild(embedWrapper);
} else if (comic.is_multi_image) {
// Multi-image comic (webtoon style) // Multi-image comic (webtoon style)
comic.filenames.forEach((filename, index) => { comic.filenames.forEach((filename, index) => {
const img = document.createElement('img'); const img = document.createElement('img');
@@ -447,13 +458,14 @@
const formattedDate = dateDisplay ? dateDisplay.textContent : null; const formattedDate = dateDisplay ? dateDisplay.textContent : null;
updateNavButtons(currentNumber, formattedDate); updateNavButtons(currentNumber, formattedDate);
// Check if current comic is multi-image // Check if current comic is multi-image or HTML embed
const comicImageDiv = document.querySelector('.comic-image'); const comicImageDiv = document.querySelector('.comic-image');
const isMultiImage = comicImageDiv && comicImageDiv.classList.contains('comic-image-multi'); const isMultiImage = comicImageDiv && comicImageDiv.classList.contains('comic-image-multi');
const isHtmlEmbed = comicImageDiv && comicImageDiv.classList.contains('comic-image-embed');
if (!isMultiImage) { if (!isMultiImage && !isHtmlEmbed) {
updateComicImageLink(currentNumber); updateComicImageLink(currentNumber);
} else { } else if (isMultiImage) {
// Initialize lazy loading for multi-image comics on page load // Initialize lazy loading for multi-image comics on page load
initLazyLoad(); initLazyLoad();
} }

View File

@@ -103,33 +103,15 @@
<div class="footer-section"> <div class="footer-section">
<h3>Follow</h3> <h3>Follow</h3>
<div class="social-links{% if use_footer_social_icons %} social-links-icons{% endif %}"> <div class="social-links{% if use_footer_social_icons %} social-links-icons{% endif %}">
{% if social_instagram %} {% for link in social_links %}
<a href="{{ social_instagram }}" target="_blank" rel="noopener noreferrer" aria-label="Instagram"> <a href="{{ link.url }}" {% if link.url.startswith('http') %}target="_blank" rel="noopener noreferrer"{% endif %} aria-label="{{ link.label }}">
{% if use_footer_social_icons %} {% if use_footer_social_icons and link.icon %}
<img src="{{ 'images/icons/instagram.png' | cdn_static }}" alt="" class="social-icon"> <img src="{{ ('images/icons/' + link.icon) | cdn_static }}" alt="" class="social-icon">
{% else %} {% else %}
Instagram {{ link.label }}
{% endif %} {% endif %}
</a> </a>
{% endif %} {% endfor %}
{% if social_youtube %}
<a href="{{ social_youtube }}" target="_blank" rel="noopener noreferrer" aria-label="YouTube">
{% if use_footer_social_icons %}
<img src="{{ 'images/icons/youtube.png' | cdn_static }}" alt="" class="social-icon">
{% else %}
YouTube
{% endif %}
</a>
{% endif %}
{% if social_email %}
<a href="{{ social_email }}" aria-label="Email">
{% if use_footer_social_icons %}
<img src="{{ 'images/icons/mail .png' | cdn_static }}" alt="" class="social-icon">
{% else %}
Email
{% endif %}
</a>
{% endif %}
<a href="{{ 'feed.rss' | cdn_static }}" aria-label="RSS Feed"> <a href="{{ 'feed.rss' | cdn_static }}" aria-label="RSS Feed">
{% if use_footer_social_icons %} {% if use_footer_social_icons %}
<img src="{{ 'images/icons/rss.png' | cdn_static }}" alt="" class="social-icon"> <img src="{{ 'images/icons/rss.png' | cdn_static }}" alt="" class="social-icon">

View File

@@ -37,8 +37,13 @@
</div> </div>
{% endif %} {% endif %}
<div class="comic-image{% if comic.is_multi_image %} comic-image-multi{% endif %}" id="comic-image-focus" tabindex="-1"> <div class="comic-image{% if comic.is_multi_image %} comic-image-multi{% endif %}{% if comic.html_embed %} comic-image-embed{% endif %}" id="comic-image-focus" tabindex="-1">
{% if comic.is_multi_image %} {% if comic.html_embed %}
{# HTML embed (video, widget, etc.) #}
<div class="comic-embed-wrapper">
{{ comic.html_embed | safe }}
</div>
{% elif comic.is_multi_image %}
{# Multi-image layout (webtoon style) - no click-through on individual images #} {# Multi-image layout (webtoon style) - no click-through on individual images #}
{% for i in range(comic.filenames|length) %} {% for i in range(comic.filenames|length) %}
<img src="{% if loop.first %}{{ ('images/comics/' + comic.filenames[i]) | cdn_static }}{% endif %}" <img src="{% if loop.first %}{{ ('images/comics/' + comic.filenames[i]) | cdn_static }}{% endif %}"

View File

@@ -16,8 +16,13 @@
<p class="comic-date">{{ comic.date }}</p> <p class="comic-date">{{ comic.date }}</p>
</div> </div>
<div class="comic-image{% if comic.is_multi_image %} comic-image-multi{% endif %}" id="comic-image-focus" tabindex="-1"> <div class="comic-image{% if comic.is_multi_image %} comic-image-multi{% endif %}{% if comic.html_embed %} comic-image-embed{% endif %}" id="comic-image-focus" tabindex="-1">
{% if comic.is_multi_image %} {% if comic.html_embed %}
{# HTML embed (video, widget, etc.) #}
<div class="comic-embed-wrapper">
{{ comic.html_embed | safe }}
</div>
{% elif comic.is_multi_image %}
{# Multi-image layout (webtoon style) - no click-through on individual images #} {# Multi-image layout (webtoon style) - no click-through on individual images #}
{% for i in range(comic.filenames|length) %} {% for i in range(comic.filenames|length) %}
<img src="{% if loop.first %}{{ ('images/comics/' + comic.filenames[i]) | cdn_static }}{% endif %}" <img src="{% if loop.first %}{{ ('images/comics/' + comic.filenames[i]) | cdn_static }}{% endif %}"

View File

@@ -2,4 +2,4 @@
# This file contains the version number for the project # This file contains the version number for the project
# Format: YYYY.MM.DD (date-based versioning) # Format: YYYY.MM.DD (date-based versioning)
__version__ = "2025.11.15" __version__ = "2025.11.18"