Compare commits
8 Commits
v2025.11.1
...
v2025.11.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b8f30ef82 | |||
| 882eed90f9 | |||
| d374df6b0b | |||
| 9c566bcc3c | |||
| 2580ea076d | |||
| b1f0f9f09b | |||
| 0381908610 | |||
| 6b3d446207 |
31
.gitignore
vendored
31
.gitignore
vendored
@@ -1,9 +1,34 @@
|
||||
# ============================================================
|
||||
# FORK-FRIENDLY GITIGNORE
|
||||
# ============================================================
|
||||
# This gitignore is designed for the fork-and-customize workflow.
|
||||
# - User content (comics, images) IS tracked in git
|
||||
# - User config (comics_data.py, variables.css) IS tracked in git
|
||||
# - Only generated/temporary files are ignored
|
||||
# ============================================================
|
||||
|
||||
# IDE and environment
|
||||
.idea
|
||||
.venv
|
||||
|
||||
# This should be generated on deploy
|
||||
# Python bytecode
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# User configuration (use the .example files as templates)
|
||||
# Uncomment these lines if you want to gitignore user config:
|
||||
# comics_data.py
|
||||
# static/css/variables.css
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
|
||||
# Generated files (regenerated on deploy/publish)
|
||||
static/feed.rss
|
||||
static/sitemap.xml
|
||||
|
||||
# Comic data cache
|
||||
data/comics/.comics_cache.pkl
|
||||
16
CHANGELOG.md
16
CHANGELOG.md
@@ -14,6 +14,19 @@ and this project uses date-based versioning (YYYY.MM.DD).
|
||||
### Fixed
|
||||
### 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
|
||||
|
||||
### Added
|
||||
@@ -25,5 +38,6 @@ and this project uses date-based versioning (YYYY.MM.DD).
|
||||
- This CHANGELOG.md file to track version history
|
||||
- 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
|
||||
|
||||
133
CLAUDE.md
133
CLAUDE.md
@@ -6,6 +6,106 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
Sunday Comics is a Flask-based webcomic website with server-side rendering and client-side navigation. Comics are stored as individual YAML files in `data/comics/`, making them easy to manage without a database. Each comic gets its own file for clean organization and version control.
|
||||
|
||||
## Fork-and-Customize Architecture
|
||||
|
||||
**IMPORTANT:** Sunday Comics is designed for users to fork and customize for their own webcomics. When making changes, maintain the separation between framework code and user customization to avoid breaking upstream updates.
|
||||
|
||||
### File Categories
|
||||
|
||||
**Core Framework Files** (Updated by upstream - DO NOT modify unless fixing bugs):
|
||||
- `app.py` - Flask application logic
|
||||
- `data_loader.py` - YAML loading and caching
|
||||
- `templates/*.html` - Jinja2 templates
|
||||
- `static/css/style.css` - Core framework styles (references CSS variables)
|
||||
- `static/js/*.js` - Client-side navigation and functionality
|
||||
- `scripts/*.py` - Utility scripts
|
||||
- `version.py`, `VERSION` - Version management
|
||||
- `Dockerfile`, `docker-compose.yml` - Deployment configuration
|
||||
|
||||
**User Customization Files** (Safe for users to modify):
|
||||
- `comics_data.py` - Global configuration settings
|
||||
- `static/css/variables.css` - Design variables (colors, fonts, spacing, layout)
|
||||
- `data/comics/*.yaml` - Comic metadata (except TEMPLATE.yaml)
|
||||
- `content/*.md` - Markdown content (about page, author notes, terms)
|
||||
- `static/images/*` - User's images and graphics
|
||||
|
||||
**Template Files** (Reference only):
|
||||
- `comics_data.py.example` - Configuration template showing all options
|
||||
- `data/comics/TEMPLATE.yaml` - Comic file template
|
||||
|
||||
**Generated Files** (Auto-created, gitignored):
|
||||
- `static/feed.rss`, `static/sitemap.xml` - Generated by scripts
|
||||
- `data/comics/.comics_cache.pkl` - Comic cache
|
||||
- `__pycache__/`, `*.pyc` - Python bytecode
|
||||
|
||||
### CSS Architecture
|
||||
|
||||
**Two-file CSS system** to separate customization from framework:
|
||||
|
||||
1. **`static/css/variables.css`** (User customization)
|
||||
- Contains all CSS custom properties
|
||||
- Organized by category: Colors, Typography, Spacing, Borders, Layout, Transitions
|
||||
- Users edit this file to customize their design
|
||||
- Loaded first in templates
|
||||
|
||||
2. **`static/css/style.css`** (Core framework)
|
||||
- References variables from variables.css
|
||||
- Contains structural styles and layout logic
|
||||
- Should not be modified by users (to allow upstream updates)
|
||||
- Loaded after variables.css
|
||||
|
||||
**When modifying styles:**
|
||||
- Add new design tokens to `variables.css` (e.g., `--color-accent: #ff0000;`)
|
||||
- Reference those variables in `style.css` (e.g., `color: var(--color-accent);`)
|
||||
- Never hardcode values in `style.css` that users might want to customize
|
||||
|
||||
### Configuration Pattern
|
||||
|
||||
**`comics_data.py.example`** serves as a reference:
|
||||
- Shows all available configuration options with defaults
|
||||
- Updated when new settings are added to the framework
|
||||
- Users can check this file when merging upstream updates
|
||||
- Never imported - purely documentation
|
||||
|
||||
**When adding new configuration options:**
|
||||
1. Add the option to `comics_data.py` with a default value
|
||||
2. Add the same option to `comics_data.py.example` with documentation
|
||||
3. Update `CHANGELOG.md` with migration instructions
|
||||
4. Consider backward compatibility (provide sensible defaults)
|
||||
|
||||
### Best Practices for Code Changes
|
||||
|
||||
**DO:**
|
||||
- ✅ Add new features to core framework files (app.py, templates, scripts)
|
||||
- ✅ Create new CSS variables in variables.css for customizable values
|
||||
- ✅ Update comics_data.py.example when adding new config options
|
||||
- ✅ Document breaking changes in CHANGELOG.md with migration steps
|
||||
- ✅ Test that user customizations (comics_data.py, variables.css) still work
|
||||
- ✅ Keep file structure consistent with the fork-friendly model
|
||||
|
||||
**DON'T:**
|
||||
- ❌ Hardcode design values in style.css that users might want to change
|
||||
- ❌ Modify user content files (data/comics/*.yaml, content/*.md)
|
||||
- ❌ Change the purpose or structure of user customization files
|
||||
- ❌ Remove configuration options without deprecation warnings
|
||||
- ❌ Make changes that require users to edit core framework files
|
||||
|
||||
**When adding new features:**
|
||||
1. Ask: "Will users want to customize this?"
|
||||
2. If yes: Add a variable/config option
|
||||
3. If no: Implement in framework code
|
||||
4. Always maintain the separation
|
||||
|
||||
### Upstream Update Workflow
|
||||
|
||||
Users following [UPSTREAM.md](UPSTREAM.md) will:
|
||||
1. Fork the repository
|
||||
2. Customize `comics_data.py` and `variables.css`
|
||||
3. Add their comics and content
|
||||
4. Periodically merge upstream updates: `git merge upstream/main`
|
||||
5. Resolve conflicts (usually only in .example files)
|
||||
6. Benefit from framework improvements without losing customizations
|
||||
|
||||
## Development Commands
|
||||
|
||||
**Run the development server:**
|
||||
@@ -114,7 +214,8 @@ Performance with caching (1000 comics):
|
||||
|
||||
Each comic YAML file contains:
|
||||
- `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
|
||||
- `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)
|
||||
@@ -124,6 +225,18 @@ Each comic YAML file contains:
|
||||
- `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.
|
||||
|
||||
**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:**
|
||||
```yaml
|
||||
filename:
|
||||
@@ -157,9 +270,8 @@ Global configuration in `comics_data.py`:
|
||||
- `USE_HEADER_NAV_ICONS`: Set to True to display icons next to main header navigation text (uses alert.png, archive.png, info.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
|
||||
- `SOCIAL_INSTAGRAM`: Instagram URL (set to None to hide)
|
||||
- `SOCIAL_YOUTUBE`: YouTube URL (set to None to hide)
|
||||
- `SOCIAL_EMAIL`: Email mailto link (set to None to hide)
|
||||
- `NEWSLETTER_HTML`: Custom HTML for newsletter form (user pastes their service's form code here)
|
||||
- `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.)
|
||||
|
||||
### Markdown Support
|
||||
|
||||
@@ -190,7 +302,8 @@ Global configuration in `comics_data.py`:
|
||||
Provides SPA-like navigation without page reloads:
|
||||
- Fetches comics from `/api/comics/<id>`
|
||||
- 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
|
||||
- Shows/hides header based on plain mode
|
||||
- Adjusts container for full_width mode
|
||||
@@ -218,12 +331,14 @@ Global context variables injected into all templates:
|
||||
- `use_header_nav_icons`: Boolean for main header navigation 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`
|
||||
- `social_instagram`: Instagram URL from `comics_data.py`
|
||||
- `social_youtube`: YouTube URL from `comics_data.py`
|
||||
- `social_email`: Email link from `comics_data.py`
|
||||
- `newsletter_html`: Custom HTML for newsletter form from `comics_data.py` (rendered with `| safe` filter)
|
||||
- `social_links`: List of social media link dicts from `comics_data.py` (each with 'label', 'url', 'icon')
|
||||
|
||||
## Static Assets
|
||||
|
||||
- `static/css/variables.css`: Design variables for user customization (colors, fonts, spacing, etc.)
|
||||
- `static/css/style.css`: Core framework styles (references variables.css)
|
||||
- `static/js/comic-nav.js`: Client-side navigation
|
||||
- `static/images/comics/`: Full-size comic images
|
||||
- `static/images/thumbs/`: Thumbnails for archive page (optional, same filename as comic)
|
||||
- `static/images/icons/`: Navigation icons (first.png, previous.png, next.png, latest.png) used when `USE_ICON_NAV` is True
|
||||
@@ -280,7 +395,7 @@ Environment variables:
|
||||
Sunday Comics follows WCAG 2.1 Level AA guidelines. When modifying the site, maintain these accessibility features:
|
||||
|
||||
### Keyboard Navigation
|
||||
- **Focus indicators**: All interactive elements have visible 3px outlines when focused (defined in `static/css/style.css`)
|
||||
- **Focus indicators**: All interactive elements have visible 3px outlines when focused (colors defined in `static/css/variables.css`, styles in `static/css/style.css`)
|
||||
- **Skip to main content**: First focusable element on every page, appears at top when focused
|
||||
- **Keyboard shortcuts**: Arrow keys (Left/Right), Home, End for comic navigation (handled in `static/js/comic-nav.js`)
|
||||
- **Focus management**: After navigation, focus programmatically moves to `#comic-image-focus` element
|
||||
|
||||
@@ -25,6 +25,9 @@ COPY --chown=appuser:appuser . .
|
||||
# Switch to non-root user
|
||||
USER appuser
|
||||
|
||||
# Generate cache, RSS feed, and sitemap during build
|
||||
RUN python scripts/publish_comic.py
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3000
|
||||
|
||||
|
||||
108
README.md
108
README.md
@@ -33,6 +33,7 @@ A Flask-based webcomic website with server-side rendering using Jinja2 templates
|
||||
- [Reporting Violations](#reporting-violations)
|
||||
- [Project Structure](#project-structure)
|
||||
- [Setup](#setup)
|
||||
- [Keeping Your Fork Up-to-Date](#keeping-your-fork-up-to-date)
|
||||
- [Environment Variables](#environment-variables)
|
||||
- [Configuration](#configuration)
|
||||
- [Global Settings](#global-settings)
|
||||
@@ -44,6 +45,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 +174,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)
|
||||
|
||||
@@ -630,12 +633,14 @@ Resources:
|
||||
```
|
||||
sunday/
|
||||
├── app.py # Main Flask application
|
||||
├── comics_data.py # Global configuration (not comic data)
|
||||
├── comics_data.py # Global configuration (your settings)
|
||||
├── comics_data.py.example # Configuration template (reference for new options)
|
||||
├── data_loader.py # YAML comic loader with caching
|
||||
├── requirements.txt # Python dependencies
|
||||
├── Dockerfile # Production Docker image
|
||||
├── docker-compose.yml # Docker Compose configuration
|
||||
├── .dockerignore # Docker build exclusions
|
||||
├── UPSTREAM.md # Guide for keeping forks up-to-date
|
||||
├── data/ # Comic data directory
|
||||
│ └── comics/ # Individual comic YAML files
|
||||
│ ├── 001.yaml # Comic #1
|
||||
@@ -661,7 +666,8 @@ sunday/
|
||||
│ └── 404.html # Error page
|
||||
└── static/ # Static files
|
||||
├── css/
|
||||
│ └── style.css # Main stylesheet
|
||||
│ ├── variables.css # Design variables (your customization)
|
||||
│ └── style.css # Core framework styles
|
||||
├── js/
|
||||
│ └── comic-nav.js # Client-side navigation
|
||||
├── images/ # Image directory
|
||||
@@ -686,6 +692,38 @@ python app.py
|
||||
|
||||
3. Visit http://127.0.0.1:3000 in your browser
|
||||
|
||||
## Keeping Your Fork Up-to-Date
|
||||
|
||||
**If you forked Sunday Comics to create your own webcomic site**, you can stay up-to-date with framework improvements while keeping your customizations.
|
||||
|
||||
Sunday Comics is designed with a **fork-and-customize workflow**:
|
||||
- ✅ **Core framework files** (app.py, templates, scripts) - updated by upstream
|
||||
- ✅ **User customization** (comics_data.py, variables.css) - your settings
|
||||
- ✅ **User content** (comics, images, markdown) - your creative work
|
||||
|
||||
When you pull upstream updates, you'll get new features and bug fixes without losing your content or design.
|
||||
|
||||
**📖 [Read the complete guide: UPSTREAM.md](UPSTREAM.md)**
|
||||
|
||||
The UPSTREAM.md guide covers:
|
||||
- Setting up your fork with an upstream remote
|
||||
- Pulling and merging updates
|
||||
- Handling merge conflicts (with examples)
|
||||
- Understanding which files to modify vs. leave alone
|
||||
- Troubleshooting common issues
|
||||
|
||||
**Quick update workflow:**
|
||||
```bash
|
||||
# Every few weeks/months:
|
||||
git fetch upstream # Get new commits
|
||||
git merge upstream/main # Merge updates
|
||||
# Resolve any conflicts if needed
|
||||
git push origin main # Push to your fork
|
||||
python scripts/publish_comic.py # Rebuild site
|
||||
```
|
||||
|
||||
**First time?** See [UPSTREAM.md](UPSTREAM.md) for the initial setup steps.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
The app can be configured via environment variables:
|
||||
@@ -724,6 +762,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 +1074,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`
|
||||
|
||||
@@ -1060,8 +1154,9 @@ For larger comic archives, consider replacing the `COMICS` list with a database:
|
||||
- Customize logo by setting `LOGO_IMAGE` and `LOGO_MODE`
|
||||
|
||||
### Styling
|
||||
- Modify `static/css/style.css` to change colors, fonts, and layout
|
||||
- Current color scheme uses blue (#3498db) and dark blue-gray (#2c3e50)
|
||||
- **Customize design:** Edit `static/css/variables.css` to change colors, fonts, spacing, and layout dimensions
|
||||
- **Framework styles:** `static/css/style.css` contains core styles that reference the variables (avoid modifying for easier upstream updates)
|
||||
- Variables are organized by category: Colors, Typography, Spacing, Borders, Layout, Transitions
|
||||
|
||||
### About Page
|
||||
- Edit `content/about.md` to add your bio and comic information (supports markdown)
|
||||
@@ -1194,7 +1289,8 @@ this will improve over time. As with data sourcing, I also hope this will be reg
|
||||
|
||||
This project's code is written primarily by AI. The application itself *does not* and *will never* have
|
||||
any AI integration. When you start up the project, it is completely standalone just like any normal website. Your
|
||||
content remains yours.
|
||||
content remains yours. In fact, the website itself does its best to protect your work from AI training! See the
|
||||
"Content Protection & AI Scraping Prevention" section above.
|
||||
|
||||
**So, why use AI?**
|
||||
|
||||
|
||||
356
UPSTREAM.md
Normal file
356
UPSTREAM.md
Normal file
@@ -0,0 +1,356 @@
|
||||
# Updating from Upstream
|
||||
|
||||
This guide explains how to keep your forked Sunday Comics site up-to-date with framework improvements while preserving your customizations.
|
||||
|
||||
## Fork-and-Customize Workflow
|
||||
|
||||
Sunday Comics is designed to be forked and customized for your own webcomic. The project separates:
|
||||
|
||||
- **Core framework files** (updated by upstream) - app logic, templates, scripts
|
||||
- **User content files** (your comic data) - comics, images, markdown content
|
||||
- **User configuration** (your settings) - config and design variables
|
||||
|
||||
When you pull updates from upstream, you'll get new features and bug fixes without losing your content or customizations.
|
||||
|
||||
---
|
||||
|
||||
## Initial Setup
|
||||
|
||||
### 1. Fork the Repository
|
||||
|
||||
On GitHub, click "Fork" to create your own copy of Sunday Comics.
|
||||
|
||||
### 2. Clone Your Fork
|
||||
|
||||
```bash
|
||||
git clone https://github.com/YOUR-USERNAME/sunday-comics.git
|
||||
cd sunday-comics
|
||||
```
|
||||
|
||||
### 3. Add Upstream Remote
|
||||
|
||||
Add the original repository as an "upstream" remote:
|
||||
|
||||
```bash
|
||||
git remote add upstream https://github.com/ORIGINAL-AUTHOR/sunday-comics.git
|
||||
```
|
||||
|
||||
Verify your remotes:
|
||||
|
||||
```bash
|
||||
git remote -v
|
||||
# origin https://github.com/YOUR-USERNAME/sunday-comics.git (fetch)
|
||||
# origin https://github.com/YOUR-USERNAME/sunday-comics.git (push)
|
||||
# upstream https://github.com/ORIGINAL-AUTHOR/sunday-comics.git (fetch)
|
||||
# upstream https://github.com/ORIGINAL-AUTHOR/sunday-comics.git (push)
|
||||
```
|
||||
|
||||
### 4. Set Up Your Configuration
|
||||
|
||||
**Option A: Track your config in git (recommended)**
|
||||
|
||||
Your `comics_data.py` and `static/css/variables.css` files are already set up. Customize them and commit:
|
||||
|
||||
```bash
|
||||
# Edit your configuration
|
||||
nano comics_data.py
|
||||
nano static/css/variables.css
|
||||
|
||||
# Commit your changes
|
||||
git add comics_data.py static/css/variables.css
|
||||
git commit -m "Configure site settings and design"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
**Option B: Keep config out of git**
|
||||
|
||||
If you prefer to keep your config private:
|
||||
|
||||
```bash
|
||||
# Uncomment the gitignore lines for config files
|
||||
nano .gitignore
|
||||
# Uncomment:
|
||||
# comics_data.py
|
||||
# static/css/variables.css
|
||||
|
||||
# Copy the example files
|
||||
cp comics_data.py comics_data.py.backup
|
||||
# Now comics_data.py won't be tracked
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Getting Updates from Upstream
|
||||
|
||||
### 1. Fetch Upstream Changes
|
||||
|
||||
```bash
|
||||
git fetch upstream
|
||||
```
|
||||
|
||||
This downloads new commits from the original repository without modifying your files.
|
||||
|
||||
### 2. Review What Changed
|
||||
|
||||
See what's new in upstream:
|
||||
|
||||
```bash
|
||||
# View commit log
|
||||
git log HEAD..upstream/main --oneline
|
||||
|
||||
# See which files changed
|
||||
git diff HEAD..upstream/main --stat
|
||||
|
||||
# Review the CHANGELOG
|
||||
git show upstream/main:CHANGELOG.md
|
||||
```
|
||||
|
||||
### 3. Merge Upstream Changes
|
||||
|
||||
```bash
|
||||
# Merge upstream into your current branch
|
||||
git merge upstream/main
|
||||
```
|
||||
|
||||
**If there are no conflicts:**
|
||||
```bash
|
||||
# Push the updates to your fork
|
||||
git push origin main
|
||||
```
|
||||
|
||||
**If there are conflicts** (see next section).
|
||||
|
||||
---
|
||||
|
||||
## Handling Merge Conflicts
|
||||
|
||||
Conflicts may occur if both you and upstream modified the same file. Common scenarios:
|
||||
|
||||
### Scenario 1: Framework Added New Config Options
|
||||
|
||||
**Example:** Upstream added a new setting to `comics_data.py.example`
|
||||
|
||||
**What to do:**
|
||||
1. The merge conflict will only be in `comics_data.py.example` (not your `comics_data.py`)
|
||||
2. Accept upstream's version:
|
||||
```bash
|
||||
git checkout --theirs comics_data.py.example
|
||||
git add comics_data.py.example
|
||||
```
|
||||
3. Review `comics_data.py.example` for new settings
|
||||
4. Manually add any new settings you want to your `comics_data.py`
|
||||
|
||||
### Scenario 2: CSS Variables Conflict
|
||||
|
||||
**Example:** Both you and upstream added/changed CSS variables
|
||||
|
||||
**What to do:**
|
||||
1. Open the conflicted file:
|
||||
```bash
|
||||
nano static/css/variables.css
|
||||
```
|
||||
2. Look for conflict markers:
|
||||
```
|
||||
<<<<<<< HEAD
|
||||
--your-custom-variable: #ff0000;
|
||||
=======
|
||||
--new-upstream-variable: #00ff00;
|
||||
>>>>>>> upstream/main
|
||||
```
|
||||
3. Keep both variables:
|
||||
```css
|
||||
--your-custom-variable: #ff0000;
|
||||
--new-upstream-variable: #00ff00;
|
||||
```
|
||||
4. Save and mark as resolved:
|
||||
```bash
|
||||
git add static/css/variables.css
|
||||
git commit -m "Merge upstream CSS variables"
|
||||
```
|
||||
|
||||
### Scenario 3: Rare - Core File Conflicts
|
||||
|
||||
If you modified a core framework file (app.py, templates, etc.), you may have conflicts.
|
||||
|
||||
**Best practice:** Avoid modifying core files. If you need custom behavior:
|
||||
- Add new routes/functions instead of modifying existing ones
|
||||
- Use template blocks for customization
|
||||
- Open an issue/PR to suggest the feature for upstream
|
||||
|
||||
**If you must resolve:**
|
||||
```bash
|
||||
# Open the conflicted file
|
||||
nano path/to/conflicted_file.py
|
||||
|
||||
# Resolve conflicts manually
|
||||
# Then:
|
||||
git add path/to/conflicted_file.py
|
||||
git commit -m "Merge upstream changes with custom modifications"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Categories Reference
|
||||
|
||||
### Core Framework (Upstream Updates)
|
||||
|
||||
**DO NOT modify these files** - you'll get conflicts when merging:
|
||||
|
||||
- `app.py` - Flask application
|
||||
- `data_loader.py` - YAML loading logic
|
||||
- `scripts/*.py` - Utility scripts
|
||||
- `templates/*.html` - Jinja templates (unless extending)
|
||||
- `static/css/style.css` - Core CSS framework
|
||||
- `static/js/*.js` - JavaScript functionality
|
||||
- `version.py`, `VERSION` - Version info
|
||||
- `Dockerfile`, `docker-compose.yml` - Deployment config
|
||||
- Documentation files (README, CLAUDE, etc.)
|
||||
|
||||
### User Configuration (You Customize)
|
||||
|
||||
**Safe to modify** - tracked in git:
|
||||
|
||||
- `comics_data.py` - Your site settings
|
||||
- `static/css/variables.css` - Your design variables
|
||||
- `data/comics/*.yaml` - Your comic metadata
|
||||
- `content/*.md` - Your markdown content (about page, author notes)
|
||||
- `static/images/*` - Your images and graphics
|
||||
|
||||
### Template Files (Reference)
|
||||
|
||||
**Use as reference** - copy to create your versions:
|
||||
|
||||
- `comics_data.py.example` - Default configuration template
|
||||
- `data/comics/TEMPLATE.yaml` - Comic file template
|
||||
|
||||
### Generated Files (Ignored by Git)
|
||||
|
||||
**Automatically regenerated:**
|
||||
|
||||
- `static/feed.rss` - RSS feed
|
||||
- `static/sitemap.xml` - Sitemap
|
||||
- `data/comics/.comics_cache.pkl` - Comic cache
|
||||
- `__pycache__/` - Python bytecode
|
||||
|
||||
---
|
||||
|
||||
## Migration Notes
|
||||
|
||||
When upstream releases breaking changes, check `CHANGELOG.md` for migration instructions.
|
||||
|
||||
**Example migration scenario:**
|
||||
|
||||
```markdown
|
||||
## [2025.03.15] - Breaking Changes
|
||||
### Changed
|
||||
- Renamed `SHOW_HEADER_IMAGE` to `HEADER_IMAGE` in comics_data.py
|
||||
|
||||
**Migration:** Update your `comics_data.py`:
|
||||
- Old: `SHOW_HEADER_IMAGE = True`
|
||||
- New: `HEADER_IMAGE = 'title.jpg'`
|
||||
```
|
||||
|
||||
After merging, check the CHANGELOG and update your config accordingly.
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### ✅ DO:
|
||||
|
||||
- Keep your fork's main branch in sync with upstream
|
||||
- Customize via `comics_data.py` and `variables.css`
|
||||
- Add your comics to `data/comics/`
|
||||
- Read the CHANGELOG before/after merging
|
||||
- Test your site after merging updates
|
||||
- Commit your changes regularly
|
||||
|
||||
### ❌ DON'T:
|
||||
|
||||
- Modify core framework files (app.py, templates, etc.)
|
||||
- Delete or rename upstream files (you'll break updates)
|
||||
- Force push over merged commits
|
||||
- Ignore merge conflicts (resolve them properly)
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "I modified a core file and now I have conflicts"
|
||||
|
||||
**Option 1: Keep your changes** (advanced)
|
||||
```bash
|
||||
# Resolve conflicts manually, testing thoroughly
|
||||
git mergetool
|
||||
git commit
|
||||
```
|
||||
|
||||
**Option 2: Discard your changes** (start fresh)
|
||||
```bash
|
||||
# Reset to upstream version
|
||||
git checkout upstream/main -- path/to/file.py
|
||||
git add path/to/file.py
|
||||
git commit -m "Reset to upstream version"
|
||||
```
|
||||
|
||||
### "I want to see what changed before merging"
|
||||
|
||||
```bash
|
||||
# Create a test branch
|
||||
git checkout -b test-upstream-merge
|
||||
|
||||
# Merge in the test branch
|
||||
git merge upstream/main
|
||||
|
||||
# If you like it, merge to main
|
||||
git checkout main
|
||||
git merge test-upstream-merge
|
||||
|
||||
# If not, delete the test branch
|
||||
git checkout main
|
||||
git branch -D test-upstream-merge
|
||||
```
|
||||
|
||||
### "My site broke after merging"
|
||||
|
||||
```bash
|
||||
# Check what changed
|
||||
git log --oneline -10
|
||||
|
||||
# Test the previous version
|
||||
git checkout HEAD~1
|
||||
python app.py
|
||||
|
||||
# If it works, the issue is in the latest commit
|
||||
git checkout main
|
||||
git diff HEAD~1 HEAD
|
||||
|
||||
# Read CHANGELOG for migration notes
|
||||
cat CHANGELOG.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Getting Help
|
||||
|
||||
- **Check the docs:** `README.md`, `CLAUDE.md`, `CHANGELOG.md`
|
||||
- **Report issues:** [GitHub Issues](https://github.com/ORIGINAL-AUTHOR/sunday-comics/issues)
|
||||
- **Ask questions:** Open a discussion or issue on GitHub
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
**Regular update workflow:**
|
||||
|
||||
```bash
|
||||
# Every few weeks/months:
|
||||
git fetch upstream # Get new commits
|
||||
git log HEAD..upstream/main --oneline # Review changes
|
||||
git merge upstream/main # Merge updates
|
||||
# Resolve any conflicts
|
||||
git push origin main # Push to your fork
|
||||
python scripts/publish_comic.py # Rebuild site
|
||||
```
|
||||
|
||||
Your comics, images, and config stay safe - only the framework code updates!
|
||||
31
app.py
31
app.py
@@ -7,10 +7,11 @@ 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
|
||||
USE_COMIC_NAV_ICONS, USE_HEADER_NAV_ICONS, USE_FOOTER_SOCIAL_ICONS, USE_SHARE_ICONS,
|
||||
NEWSLETTER_ENABLED, NEWSLETTER_HTML,
|
||||
SOCIAL_LINKS, API_SPEC_LINK, EMBED_ENABLED, PERMALINK_ENABLED
|
||||
)
|
||||
import markdown
|
||||
from version import __version__
|
||||
@@ -32,6 +33,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 +59,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,
|
||||
@@ -53,9 +73,8 @@ def inject_global_settings():
|
||||
'use_footer_social_icons': USE_FOOTER_SOCIAL_ICONS,
|
||||
'use_share_icons': USE_SHARE_ICONS,
|
||||
'newsletter_enabled': NEWSLETTER_ENABLED,
|
||||
'social_instagram': SOCIAL_INSTAGRAM,
|
||||
'social_youtube': SOCIAL_YOUTUBE,
|
||||
'social_email': SOCIAL_EMAIL,
|
||||
'newsletter_html': NEWSLETTER_HTML,
|
||||
'social_links': SOCIAL_LINKS,
|
||||
'api_spec_link': API_SPEC_LINK,
|
||||
'embed_enabled': EMBED_ENABLED,
|
||||
'permalink_enabled': PERMALINK_ENABLED,
|
||||
|
||||
@@ -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
|
||||
@@ -80,10 +86,30 @@ USE_SHARE_ICONS = True
|
||||
# Global setting: Set to True to show newsletter section in footer
|
||||
NEWSLETTER_ENABLED = False
|
||||
|
||||
# Social media links - set to None to hide the link
|
||||
SOCIAL_INSTAGRAM = None # e.g., 'https://instagram.com/yourhandle'
|
||||
SOCIAL_YOUTUBE = None # e.g., 'https://youtube.com/@yourchannel'
|
||||
SOCIAL_EMAIL = None # e.g., 'mailto:your@email.com'
|
||||
# Global setting: Custom HTML for newsletter form (only used if NEWSLETTER_ENABLED is True)
|
||||
# Paste your newsletter service's form HTML here (Mailchimp, ConvertKit, Buttondown, etc.)
|
||||
# Example:
|
||||
# NEWSLETTER_HTML = '''
|
||||
# <form action="https://yourservice.com/subscribe" method="post" class="newsletter-form">
|
||||
# <input type="email" name="email" placeholder="Enter your email" required>
|
||||
# <button type="submit">Subscribe</button>
|
||||
# </form>
|
||||
# '''
|
||||
NEWSLETTER_HTML = '<p class="newsletter-placeholder">Newsletter coming soon!</p>'
|
||||
|
||||
# Social media links - add/remove/reorder as needed
|
||||
# Each link should have 'label', 'url', and optionally 'icon' (filename in static/images/icons/)
|
||||
# Set to empty list [] to show no social links
|
||||
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
|
||||
# Path is relative to static/ directory
|
||||
|
||||
139
comics_data.py.example
Normal file
139
comics_data.py.example
Normal file
@@ -0,0 +1,139 @@
|
||||
# Sunday Comics - Comic data configuration
|
||||
# Copyright (c) 2025 Tomasita Cabrera
|
||||
# Licensed under the MIT License - see LICENSE file for details
|
||||
#
|
||||
# Edit this file to add, remove, or modify comics
|
||||
|
||||
# Global setting: The name of your comic/website
|
||||
COMIC_NAME = 'Sunday Comics'
|
||||
|
||||
# Global setting: The name to display in the copyright notice
|
||||
# If not set (None), defaults to COMIC_NAME
|
||||
COPYRIGHT_NAME = None # e.g., 'Your Name' or 'Your Studio Name'
|
||||
|
||||
# Global setting: Your website's domain (used for RSS feed, Open Graph tags, etc.)
|
||||
# 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
|
||||
|
||||
# Global setting: Set to True to hide header and remove nav border by default
|
||||
# Individual comics can override this with 'plain': False
|
||||
PLAIN_DEFAULT = False
|
||||
|
||||
# Global setting: Path to site logo (relative to static/images/)
|
||||
# Set to None to disable logo
|
||||
# Example: LOGO_IMAGE = 'logo.png' will use static/images/logo.png
|
||||
LOGO_IMAGE = 'logo.png'
|
||||
|
||||
# Global setting: Logo display mode
|
||||
# 'beside' - Display logo next to the site title
|
||||
# 'replace' - Replace the site title with the logo
|
||||
# Only applies when LOGO_IMAGE is set
|
||||
LOGO_MODE = 'beside'
|
||||
|
||||
# Global setting: Path to header image (relative to static/images/)
|
||||
# Set to None to disable header image
|
||||
# Example: HEADER_IMAGE = 'title.jpg' will use static/images/title.jpg
|
||||
HEADER_IMAGE = None
|
||||
|
||||
# Global setting: Path to footer image (relative to static/images/)
|
||||
# Set to None to disable footer image
|
||||
# Example: FOOTER_IMAGE = 'footer.jpg' will use static/images/footer.jpg
|
||||
FOOTER_IMAGE = None
|
||||
|
||||
# Global setting: Path to shareable banner image (relative to static/images/)
|
||||
# Set to None to disable "Link to Us" section in footer
|
||||
# Example: BANNER_IMAGE = 'banner.jpg' will use static/images/banner.jpg
|
||||
BANNER_IMAGE = 'banner.jpg'
|
||||
|
||||
# Global setting: Set to True to display footer in compact mode
|
||||
# Compact mode: single line, no border, horizontal layout
|
||||
COMPACT_FOOTER = False
|
||||
|
||||
# Global setting: Set to True to make archive page full-width with 4 columns (2 on mobile)
|
||||
# Full-width archive shows square thumbnails with only dates, no titles
|
||||
ARCHIVE_FULL_WIDTH = True
|
||||
|
||||
# Global setting: Set to True to enable sections/chapters on the archive page
|
||||
# Add 'section': 'Chapter Title' to comics where a new section starts
|
||||
SECTIONS_ENABLED = True
|
||||
|
||||
# Global setting: Set to True to use icon images for comic navigation buttons
|
||||
# Icons should be in static/images/icons/ (first.png, previous.png, next.png, latest.png)
|
||||
USE_COMIC_NAV_ICONS = True
|
||||
|
||||
# Global setting: Set to True to show icons next to main header navigation text
|
||||
# Uses alert.png for Latest, archive.png for Archive, info.png for About
|
||||
USE_HEADER_NAV_ICONS = True
|
||||
|
||||
# Global setting: Set to True to use icons instead of text for footer social links
|
||||
# Uses instagram.png, youtube.png, and mail.png from static/images/icons/
|
||||
USE_FOOTER_SOCIAL_ICONS = True
|
||||
|
||||
# Global setting: Set to True to show icons in share buttons (permalink and embed)
|
||||
# Uses link.png for permalink and embed.png for embed from static/images/icons/
|
||||
USE_SHARE_ICONS = True
|
||||
|
||||
# Global setting: Set to True to show newsletter section in footer
|
||||
NEWSLETTER_ENABLED = False
|
||||
|
||||
# Global setting: Custom HTML for newsletter form (only used if NEWSLETTER_ENABLED is True)
|
||||
# Paste your newsletter service's form HTML here (Mailchimp, ConvertKit, Buttondown, etc.)
|
||||
# Example:
|
||||
# NEWSLETTER_HTML = '''
|
||||
# <form action="https://yourservice.com/subscribe" method="post" class="newsletter-form">
|
||||
# <input type="email" name="email" placeholder="Enter your email" required>
|
||||
# <button type="submit">Subscribe</button>
|
||||
# </form>
|
||||
# '''
|
||||
NEWSLETTER_HTML = '<p class="newsletter-placeholder">Newsletter coming soon!</p>'
|
||||
|
||||
# Social media links - add/remove/reorder as needed
|
||||
# Each link should have 'label', 'url', and optionally 'icon' (filename in static/images/icons/)
|
||||
# Set to empty list [] to show no social links
|
||||
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
|
||||
# Path is relative to static/ directory
|
||||
API_SPEC_LINK = None # Set to 'openapi.yml' to enable
|
||||
|
||||
# Global setting: Set to True to enable comic embed functionality
|
||||
# When enabled, users can get embed codes to display comics on other websites
|
||||
EMBED_ENABLED = True
|
||||
|
||||
# Global setting: Set to True to enable permalink copy button
|
||||
# When enabled, users can easily copy a direct link to the current comic
|
||||
PERMALINK_ENABLED = True
|
||||
|
||||
# Load comics from YAML files
|
||||
from data_loader import load_comics_from_yaml, validate_comics
|
||||
|
||||
COMICS = load_comics_from_yaml('data/comics')
|
||||
|
||||
# Validate loaded comics
|
||||
if not validate_comics(COMICS):
|
||||
print("Warning: Comic validation failed. Please check your YAML files.")
|
||||
|
||||
# Show loaded comics count
|
||||
if COMICS:
|
||||
print(f"Loaded {len(COMICS)} comics from data/comics/")
|
||||
else:
|
||||
print("Warning: No comics loaded! Please add .yaml files to data/comics/")
|
||||
@@ -6,7 +6,7 @@
|
||||
# REQUIRED: Sequential comic number
|
||||
number: 999
|
||||
|
||||
# REQUIRED: Image filename(s) in static/images/comics/
|
||||
# REQUIRED (unless using html_embed): Image filename(s) in static/images/comics/
|
||||
# Single image:
|
||||
filename: comic-999.jpg
|
||||
# OR multi-image (webtoon style):
|
||||
@@ -18,6 +18,12 @@ filename: comic-999.jpg
|
||||
# Optional: Mobile-optimized version of the comic
|
||||
# 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)
|
||||
date: "2025-01-01"
|
||||
|
||||
|
||||
@@ -1,55 +1,13 @@
|
||||
/* CSS Variables for easy customization */
|
||||
:root {
|
||||
/* Colors */
|
||||
--color-primary: #000;
|
||||
--color-background: #fff;
|
||||
--color-text: #000;
|
||||
--color-text-muted: #666;
|
||||
--color-disabled: #999;
|
||||
--color-hover-bg: #f0f0f0;
|
||||
/* ============================================================
|
||||
SUNDAY COMICS - CORE STYLES
|
||||
============================================================
|
||||
|
||||
/* Typography */
|
||||
--font-family: 'Courier New', Courier, monospace;
|
||||
--font-size-base: 1rem;
|
||||
--font-size-xs: 0.7rem;
|
||||
--font-size-sm: 0.75rem;
|
||||
--font-size-md: 0.85rem;
|
||||
--font-size-lg: 0.9rem;
|
||||
--font-size-xl: 1.2rem;
|
||||
--font-size-2xl: 1.5rem;
|
||||
--font-size-3xl: 2rem;
|
||||
--font-size-4xl: 6rem;
|
||||
--line-height-base: 1.5;
|
||||
--line-height-tight: 1.3;
|
||||
--line-height-relaxed: 1.6;
|
||||
--letter-spacing-tight: 1px;
|
||||
--letter-spacing-wide: 2px;
|
||||
This file contains the framework CSS that references variables
|
||||
defined in variables.css. When updating from upstream, this file
|
||||
may change, but your customizations in variables.css will be preserved.
|
||||
|
||||
/* Spacing */
|
||||
--space-xs: 0.25rem;
|
||||
--space-sm: 0.5rem;
|
||||
--space-md: 1rem;
|
||||
--space-lg: 1.5rem;
|
||||
--space-xl: 2rem;
|
||||
--space-2xl: 3rem;
|
||||
--space-3xl: 4rem;
|
||||
|
||||
/* Borders */
|
||||
--border-width-thin: 2px;
|
||||
--border-width-thick: 3px;
|
||||
--border-color: var(--color-primary);
|
||||
--border-radius: 0; /* Can be changed for rounded corners */
|
||||
|
||||
/* Layout */
|
||||
--container-max-width: 900px;
|
||||
--content-max-width: 700px;
|
||||
--archive-grid-min: 180px;
|
||||
--archive-grid-min-mobile: 140px;
|
||||
--archive-thumbnail-height: 120px;
|
||||
|
||||
/* Transitions (for future enhancements) */
|
||||
--transition-speed: 0.2s;
|
||||
}
|
||||
Do not edit this file directly for design changes - use variables.css instead.
|
||||
============================================================ */
|
||||
|
||||
/* Reset and base styles */
|
||||
* {
|
||||
|
||||
80
static/css/variables.css
Normal file
80
static/css/variables.css
Normal file
@@ -0,0 +1,80 @@
|
||||
/* ============================================================
|
||||
CSS VARIABLES FOR CUSTOMIZATION
|
||||
============================================================
|
||||
|
||||
This file contains all customizable design tokens for your webcomic.
|
||||
Edit these values to change colors, fonts, spacing, and layout dimensions.
|
||||
|
||||
The main style.css file references these variables, so you can update
|
||||
your design without touching the core framework styles.
|
||||
============================================================ */
|
||||
|
||||
:root {
|
||||
/* ========================================
|
||||
COLORS
|
||||
======================================== */
|
||||
|
||||
--color-primary: #000;
|
||||
--color-background: #fff;
|
||||
--color-text: #000;
|
||||
--color-text-muted: #666;
|
||||
--color-disabled: #999;
|
||||
--color-hover-bg: #f0f0f0;
|
||||
|
||||
/* ========================================
|
||||
TYPOGRAPHY
|
||||
======================================== */
|
||||
|
||||
--font-family: 'Courier New', Courier, monospace;
|
||||
--font-size-base: 1rem;
|
||||
--font-size-xs: 0.7rem;
|
||||
--font-size-sm: 0.75rem;
|
||||
--font-size-md: 0.85rem;
|
||||
--font-size-lg: 0.9rem;
|
||||
--font-size-xl: 1.2rem;
|
||||
--font-size-2xl: 1.5rem;
|
||||
--font-size-3xl: 2rem;
|
||||
--font-size-4xl: 6rem;
|
||||
--line-height-base: 1.5;
|
||||
--line-height-tight: 1.3;
|
||||
--line-height-relaxed: 1.6;
|
||||
--letter-spacing-tight: 1px;
|
||||
--letter-spacing-wide: 2px;
|
||||
|
||||
/* ========================================
|
||||
SPACING
|
||||
======================================== */
|
||||
|
||||
--space-xs: 0.25rem;
|
||||
--space-sm: 0.5rem;
|
||||
--space-md: 1rem;
|
||||
--space-lg: 1.5rem;
|
||||
--space-xl: 2rem;
|
||||
--space-2xl: 3rem;
|
||||
--space-3xl: 4rem;
|
||||
|
||||
/* ========================================
|
||||
BORDERS
|
||||
======================================== */
|
||||
|
||||
--border-width-thin: 2px;
|
||||
--border-width-thick: 3px;
|
||||
--border-color: var(--color-primary);
|
||||
--border-radius: 0; /* Can be changed for rounded corners */
|
||||
|
||||
/* ========================================
|
||||
LAYOUT
|
||||
======================================== */
|
||||
|
||||
--container-max-width: 900px;
|
||||
--content-max-width: 700px;
|
||||
--archive-grid-min: 180px;
|
||||
--archive-grid-min-mobile: 140px;
|
||||
--archive-thumbnail-height: 120px;
|
||||
|
||||
/* ========================================
|
||||
TRANSITIONS
|
||||
======================================== */
|
||||
|
||||
--transition-speed: 0.2s;
|
||||
}
|
||||
@@ -80,8 +80,8 @@
|
||||
const comicImageDiv = document.querySelector('.comic-image');
|
||||
updateComicImage(comicImageDiv, comic, title);
|
||||
|
||||
// Update or create/remove the link wrapper (only for single-image comics)
|
||||
if (!comic.is_multi_image) {
|
||||
// Update or create/remove the link wrapper (only for single-image comics, not HTML embeds)
|
||||
if (!comic.is_multi_image && !comic.html_embed) {
|
||||
updateComicImageLink(comic.number);
|
||||
}
|
||||
|
||||
@@ -181,15 +181,26 @@
|
||||
// Clear all existing content
|
||||
comicImageDiv.innerHTML = '';
|
||||
|
||||
// Update container class for multi-image
|
||||
if (comic.is_multi_image) {
|
||||
// Update container classes
|
||||
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.remove('comic-image-embed');
|
||||
} else {
|
||||
comicImageDiv.classList.remove('comic-image-multi');
|
||||
comicImageDiv.classList.remove('comic-image-embed');
|
||||
}
|
||||
|
||||
// Create new image element(s)
|
||||
if (comic.is_multi_image) {
|
||||
// Create new content
|
||||
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)
|
||||
comic.filenames.forEach((filename, index) => {
|
||||
const img = document.createElement('img');
|
||||
@@ -447,13 +458,14 @@
|
||||
const formattedDate = dateDisplay ? dateDisplay.textContent : null;
|
||||
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 isMultiImage = comicImageDiv && comicImageDiv.classList.contains('comic-image-multi');
|
||||
const isHtmlEmbed = comicImageDiv && comicImageDiv.classList.contains('comic-image-embed');
|
||||
|
||||
if (!isMultiImage) {
|
||||
if (!isMultiImage && !isHtmlEmbed) {
|
||||
updateComicImageLink(currentNumber);
|
||||
} else {
|
||||
} else if (isMultiImage) {
|
||||
// Initialize lazy loading for multi-image comics on page load
|
||||
initLazyLoad();
|
||||
}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -31,13 +31,16 @@
|
||||
<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') }}">
|
||||
<!-- CSS Variables (user customization) loaded first -->
|
||||
<link rel="stylesheet" href="{{ 'css/variables.css' | cdn_static }}">
|
||||
<!-- Core framework styles (references variables) -->
|
||||
<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 +49,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 +60,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 +73,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>
|
||||
@@ -100,42 +103,24 @@
|
||||
<div class="footer-section">
|
||||
<h3>Follow</h3>
|
||||
<div class="social-links{% if use_footer_social_icons %} social-links-icons{% endif %}">
|
||||
{% 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">
|
||||
{% for link in social_links %}
|
||||
<a href="{{ link.url }}" {% if link.url.startswith('http') %}target="_blank" rel="noopener noreferrer"{% endif %} aria-label="{{ link.label }}">
|
||||
{% if use_footer_social_icons and link.icon %}
|
||||
<img src="{{ ('images/icons/' + link.icon) | cdn_static }}" alt="" class="social-icon">
|
||||
{% else %}
|
||||
Instagram
|
||||
{{ link.label }}
|
||||
{% endif %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if social_youtube %}
|
||||
<a href="{{ social_youtube }}" target="_blank" rel="noopener noreferrer" aria-label="YouTube">
|
||||
{% endfor %}
|
||||
<a href="{{ 'feed.rss' | cdn_static }}" aria-label="RSS Feed">
|
||||
{% if use_footer_social_icons %}
|
||||
<img src="{{ url_for('static', filename='images/icons/youtube.png') }}" 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="{{ url_for('static', filename='images/icons/mail .png') }}" alt="" class="social-icon">
|
||||
{% else %}
|
||||
Email
|
||||
{% endif %}
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('static', filename='feed.rss') }}" 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>
|
||||
@@ -143,12 +128,7 @@
|
||||
{% if newsletter_enabled %}
|
||||
<div class="footer-section">
|
||||
<h3>Newsletter</h3>
|
||||
<!-- Replace with your newsletter service form -->
|
||||
<!-- <form class="newsletter-form" action="#" method="post">
|
||||
<input type="email" name="email" placeholder="Enter your email" required>
|
||||
<button type="submit">Subscribe</button>
|
||||
</form> -->
|
||||
<p class="newsletter-placeholder">Newsletter coming soon!</p>
|
||||
{{ newsletter_html | safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -157,11 +137,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 +155,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 +164,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 +186,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>
|
||||
|
||||
@@ -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",
|
||||
@@ -37,12 +37,17 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="comic-image{% if comic.is_multi_image %} comic-image-multi{% endif %}" id="comic-image-focus" tabindex="-1">
|
||||
{% if comic.is_multi_image %}
|
||||
<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.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 #}
|
||||
{% 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 +58,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 +73,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 +94,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 +112,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 +152,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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 %}
|
||||
@@ -16,12 +16,17 @@
|
||||
<p class="comic-date">{{ comic.date }}</p>
|
||||
</div>
|
||||
|
||||
<div class="comic-image{% if comic.is_multi_image %} comic-image-multi{% endif %}" id="comic-image-focus" tabindex="-1">
|
||||
{% if comic.is_multi_image %}
|
||||
<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.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 #}
|
||||
{% 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 +34,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 +53,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 +71,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 +111,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>
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
# This file contains the version number for the project
|
||||
# Format: YYYY.MM.DD (date-based versioning)
|
||||
|
||||
__version__ = "2025.11.15"
|
||||
__version__ = "2025.11.18"
|
||||
|
||||
Reference in New Issue
Block a user