Compare commits

...

4 Commits

Author SHA1 Message Date
mi
631bca7923 📝 initial release docs 2025-11-15 20:53:31 +10:00
mi
6dad2194a5 🔖 initial release 2025-11-15 20:46:44 +10:00
mi
3153455355 📝 consolidate multiple list endpoints for comics 2025-11-15 20:40:18 +10:00
mi
52b80563ba 📝 catch the docs up 2025-11-15 20:21:19 +10:00
10 changed files with 586 additions and 154 deletions

29
CHANGELOG.md Normal file
View File

@@ -0,0 +1,29 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project uses date-based versioning (YYYY.MM.DD).
## [Unreleased]
### Added
### Changed
### Deprecated
### Removed
### Fixed
### Security
## [2025.11.15] - 2025-11-15
### Added
- Initial version tracking system
- `version.py` module for source code version reference
- `VERSION` file at project root for easy access
- HTML meta tag (`generator`) displaying version in page source
- Version number injected into all template contexts
- 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
[2025.11.15]: https://git.puercito.net/mi/sunday/releases/tag/v2025.11.15

View File

@@ -56,6 +56,33 @@ python scripts/rebuild_cache.py
```
Force rebuild the comics cache from YAML files. Normally not needed (cache auto-invalidates).
**Bump version:**
```bash
python scripts/bump_version.py
```
Updates the project version to today's date (YYYY.MM.DD format) in both `version.py` and `VERSION` files. Optionally opens CHANGELOG.md for editing.
**Bump version to specific date:**
```bash
python scripts/bump_version.py 2025.12.25
```
Sets version to a specific date instead of using today's date.
## Versioning
The project uses date-based versioning (YYYY.MM.DD format):
- **`version.py`**: Python module containing `__version__` variable (import with `from version import __version__`)
- **`VERSION`**: Plain text file at project root for easy access by scripts and CI/CD
- **`CHANGELOG.md`**: Tracks version history and changes following [Keep a Changelog](https://keepachangelog.com/) format
- **HTML meta tag**: Version appears in page source as `<meta name="generator" content="Sunday Comics X.Y.Z">`
When releasing a new version:
1. Run `python scripts/bump_version.py` to update version files
2. Edit `CHANGELOG.md` to document changes under the new version
3. Commit changes: `git commit -m "Release version YYYY.MM.DD"`
4. Tag the release: `git tag -a vYYYY.MM.DD -m "Version YYYY.MM.DD"`
5. Push with tags: `git push && git push --tags`
## Architecture
### Data Layer: YAML Files in data/comics/

260
README.md
View File

@@ -72,7 +72,7 @@ A Flask-based webcomic website with server-side rendering using Jinja2 templates
- An archive page where readers can browse all your comics
- RSS feed so readers can subscribe to updates
- Mobile-friendly design that works on phones and tablets
- No database required - just upload images and edit a simple text file
- No database required - just upload images and edit simple YAML files
**Perfect for:**
- Independent comic artists starting their first webcomic
@@ -81,7 +81,7 @@ A Flask-based webcomic website with server-side rendering using Jinja2 templates
- Anyone looking for a lightweight, customizable comic platform
**How it works:**
You add your comics by uploading image files and adding basic information (title, date, description) to a configuration file. The website handles everything else - displaying comics with navigation, creating an archive, generating an RSS feed, and making your comics shareable on social media.
You add your comics by uploading image files and creating individual YAML files with basic information (title, date, description). The website handles everything else - displaying comics with navigation, creating an archive, generating an RSS feed, and making your comics shareable on social media.
No coding knowledge required for basic use - just follow the instructions below to add comics and customize your site's appearance.
@@ -91,7 +91,7 @@ No coding knowledge required for basic use - just follow the instructions below
**Sunday Comics:**
- Server-side application (Flask/Python) that runs on a web server
- Comics stored in a Python file - edit text to add comics
- Comics stored as individual YAML files - easy version control and management
- Includes an RSS feed generator and helper scripts
- API endpoints for programmatic access
- Markdown support for rich-formatted content
@@ -100,7 +100,7 @@ No coding knowledge required for basic use - just follow the instructions below
**Rarebit:**
- Purely static HTML/CSS/JavaScript files
- Comics are inferred from static images upload - edit a JS to customize
- Comics are inferred from static images upload - edit a JS file to customize
- Can be hosted for free on GitHub Pages, Neocities, etc.
- No server or programming language required
- Simpler deployment - just upload files
@@ -233,13 +233,11 @@ To test keyboard navigation on your site:
When adding comics to your site, follow these guidelines to maintain accessibility:
1. **Always provide alt text**
```python
{
'number': 1,
'filename': 'comic-001.png',
'alt_text': 'A descriptive summary of what happens in the comic', # Required!
# ...
}
```yaml
number: 1
filename: comic-001.png
alt_text: A descriptive summary of what happens in the comic # Required!
date: '2025-01-01'
```
2. **Write meaningful alt text**
@@ -632,16 +630,27 @@ Resources:
```
sunday/
├── app.py # Main Flask application
├── comics_data.py # Comic data and configuration
├── comics_data.py # Global configuration (not comic data)
├── 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
├── data/ # Comic data directory
│ └── comics/ # Individual comic YAML files
│ ├── 001.yaml # Comic #1
│ ├── 002.yaml # Comic #2
│ ├── TEMPLATE.yaml # Template for new comics
│ └── .comics_cache.pkl # Auto-generated cache file
├── scripts/ # Utility scripts
│ ├── add_comic.py # Script to add new comic entries
── generate_rss.py # Script to generate RSS feed
│ ├── add_comic.py # Create new comic YAML files
── generate_rss.py # Generate RSS feed
│ ├── generate_sitemap.py # Generate sitemap.xml
│ ├── rebuild_cache.py # Force rebuild comics cache
│ └── publish_comic.py # Rebuild cache + RSS + sitemap
├── content/ # Markdown content
│ ├── about.md # About page content
│ ├── terms.md # Terms of Service
│ └── author_notes/ # Author notes for comics (by date)
├── templates/ # Jinja2 templates
│ ├── base.html # Base template with navigation
@@ -659,7 +668,8 @@ sunday/
│ ├── comics/ # Comic images
│ ├── thumbs/ # Thumbnail images for archive
│ └── icons/ # Navigation and social icons (optional)
── feed.rss # RSS feed (generated)
── feed.rss # RSS feed (generated)
└── sitemap.xml # Sitemap (generated)
```
## Setup
@@ -683,6 +693,7 @@ The app can be configured via environment variables:
- `SECRET_KEY` - Flask secret key (defaults to 'your-secret-key')
- `PORT` - Port to run on (defaults to 3000)
- `DEBUG` - Enable debug mode (defaults to False)
- `DISABLE_COMIC_CACHE` - Set to 'true' to disable comic caching (useful for debugging)
**Development:**
```bash
@@ -697,9 +708,15 @@ export PORT=3000
python app.py
```
**Disable caching (debugging):**
```bash
export DISABLE_COMIC_CACHE=true
python app.py
```
## Configuration
The `comics_data.py` file contains both comic data and global configuration options:
The `comics_data.py` file contains global configuration options for your comic site. Comic data itself is stored in individual YAML files in the `data/comics/` directory.
### Global Settings
@@ -716,10 +733,14 @@ FOOTER_IMAGE = None # Optional footer image path
BANNER_IMAGE = 'banner.jpg' # Shareable banner for "Link to Me" section
COMPACT_FOOTER = False # Display footer in compact mode
ARCHIVE_FULL_WIDTH = True # Full-width archive with 4 columns
SECTIONS_ENABLED = True # Enable section headers on archive page
USE_COMIC_NAV_ICONS = True # Use icons for comic navigation buttons
USE_HEADER_NAV_ICONS = True # Show icons in main header navigation
USE_FOOTER_SOCIAL_ICONS = True # Use icons for social links
USE_SHARE_ICONS = True # Use icons in share buttons (permalink/embed)
NEWSLETTER_ENABLED = False # Show newsletter section in footer
EMBED_ENABLED = True # Enable comic embed functionality
PERMALINK_ENABLED = True # Enable permalink copy button
SOCIAL_INSTAGRAM = None # Instagram URL (or None)
SOCIAL_YOUTUBE = None # YouTube URL (or None)
SOCIAL_EMAIL = None # Email mailto link (or None)
@@ -728,78 +749,107 @@ API_SPEC_LINK = None # API documentation link (or None)
## Adding Comics
Comics are stored in the `COMICS` list in `comics_data.py`. Each comic entry:
Comics are stored as individual YAML files in the `data/comics/` directory. Each comic file contains:
```python
{
'number': 1, # Comic number (required, sequential)
'filename': 'comic-001.png', # Image filename (required) OR list for multi-image
'mobile_filename': 'comic-001-mobile.png', # Optional mobile version (single-image only)
'date': '2025-01-01', # Publication date (required)
'alt_text': 'Alt text for comic', # Accessibility text (required) OR list for multi-image
'title': 'Comic Title', # Title (optional, shows #X if absent)
'author_note': 'Optional note', # Author note (optional, plain text)
'author_note_md': '2025-01-01.md', # Optional markdown file for author note
'full_width': True, # Optional: override FULL_WIDTH_DEFAULT
'plain': True # Optional: override PLAIN_DEFAULT
}
```yaml
number: 1 # Comic number (required, sequential)
filename: comic-001.png # Image filename (required) OR list for multi-image
date: '2025-01-01' # Publication date (required)
alt_text: Alt text for comic # Accessibility text (required) OR list for multi-image
title: Comic Title # Title (optional, shows #X if absent)
author_note: Optional note # Author note (optional, plain text)
author_note_md: 2025-01-01.md # Optional markdown file for author note
full_width: true # Optional: override FULL_WIDTH_DEFAULT
plain: true # Optional: override PLAIN_DEFAULT
```
**For multi-image comics (webtoon style):**
```python
{
'number': 2,
'filename': ['page1.png', 'page2.png', 'page3.png'], # List of images
'alt_text': ['Panel 1 description', 'Panel 2', 'Panel 3'], # Individual alt texts
'date': '2025-01-08',
'full_width': True # Recommended for webtoons
}
```yaml
number: 2
filename:
- page1.png
- page2.png
- page3.png
alt_text:
- Panel 1 description
- Panel 2 description
- Panel 3 description
date: '2025-01-08'
full_width: true # Recommended for webtoons
```
### Adding a New Comic
**Option 1: Use the script (recommended)**
```bash
# Add comic entry only
# Add comic YAML file with defaults
python scripts/add_comic.py
# Add comic entry AND create markdown file for author notes
# Add comic YAML file AND create markdown file for author notes
python scripts/add_comic.py -m
```
This will automatically add a new entry with defaults. The `-m` flag creates a markdown file in `content/author_notes/{date}.md` with a template and adds the `author_note_md` field to the comic entry. Then edit `comics_data.py` to customize.
This will create a new YAML file in `data/comics/` with the next comic number and reasonable defaults. The `-m` flag also creates a markdown file in `content/author_notes/{date}.md` with a template. Then:
1. Edit the generated YAML file to customize title, alt_text, author_note, etc.
2. Upload your comic image to `static/images/comics/`
3. Optionally create a thumbnail in `static/images/thumbs/` with the same filename
**Option 2: Manual**
1. Save your comic image in `static/images/comics/` (e.g., `comic-001.png`)
2. Optionally, create a thumbnail in `static/images/thumbs/` with the same filename
3. Add the comic entry to the `COMICS` list in `comics_data.py`
1. Copy `data/comics/TEMPLATE.yaml` and rename it (e.g., `003.yaml`)
2. Edit the YAML file to set comic properties
3. Save your comic image in `static/images/comics/`
4. Optionally, create a thumbnail in `static/images/thumbs/` with the same filename
### Generating RSS Feed
### Publishing Comics
After adding comics, regenerate the RSS feed:
After adding or updating comics, use the publish script to update all generated files:
```bash
python scripts/generate_rss.py
python scripts/publish_comic.py
```
This creates/updates `static/feed.rss`
This convenience script rebuilds the cache and regenerates both RSS feed and sitemap in one command.
### Comic Caching System
Sunday Comics uses an automatic caching system to speed up comic loading:
**How it works:**
- **First load**: Parses all YAML files, saves to `data/comics/.comics_cache.pkl`
- **Subsequent loads**: Reads from cache (~100x faster)
- **Auto-invalidation**: Cache rebuilds automatically when any YAML file is modified
**Performance (1000 comics):**
- Initial load: ~2-3 seconds (builds cache)
- Subsequent loads: ~0.01 seconds (uses cache)
- Scripts share the same cache file on disk
**Manual cache management:**
```bash
# Force rebuild the cache (normally not needed)
python scripts/rebuild_cache.py
# Disable caching (for debugging)
export DISABLE_COMIC_CACHE=true
python app.py
```
The cache file is automatically excluded from git (listed in `.gitignore`).
### Markdown Support
**Author Notes:**
Add the `author_note_md` field to your comic entry in `comics_data.py` to use markdown-formatted author notes. The field can be:
- Just a filename (e.g., `"2025-01-01.md"`) - looked up in `content/author_notes/`
- A path relative to `content/` (e.g., `"special/intro.md"`)
Add the `author_note_md` field to your comic YAML file to use markdown-formatted author notes. The field can be:
- Just a filename (e.g., `2025-01-01.md`) - looked up in `content/author_notes/`
- A path relative to `content/` (e.g., `special/intro.md`)
Markdown author notes take precedence over the plain text `author_note` field and render as HTML.
Example:
```python
# In comics_data.py
{
'number': 1,
'filename': 'comic-001.png',
'date': '2025-01-01',
'alt_text': 'First comic',
'author_note_md': '2025-01-01.md' # References content/author_notes/2025-01-01.md
}
```yaml
# In data/comics/001.yaml
number: 1
filename: comic-001.png
date: '2025-01-01'
alt_text: First comic
author_note_md: 2025-01-01.md # References content/author_notes/2025-01-01.md
```
```bash
@@ -823,31 +873,34 @@ Sunday Comics supports vertical scrolling comics with multiple images stacked se
- No click-through navigation on multi-image comics (use navigation buttons instead)
**Basic Example:**
```python
# In comics_data.py
{
'number': 4,
'title': 'Webtoon Episode 1',
'filename': ['page1.jpg', 'page2.jpg', 'page3.jpg'], # List of images
'alt_text': 'A three-part vertical story', # Single alt text for all images
'date': '2025-01-22',
}
```yaml
# In data/comics/004.yaml
number: 4
title: Webtoon Episode 1
filename:
- page1.jpg
- page2.jpg
- page3.jpg
alt_text: A three-part vertical story # Single alt text for all images
date: '2025-01-22'
```
**Individual Alt Text (Recommended for Accessibility):**
```python
{
'number': 5,
'title': 'Long Scroll Episode',
'filename': ['scene1.png', 'scene2.png', 'scene3.png', 'scene4.png'],
'alt_text': [
'Opening scene showing the city at dawn',
'Character walking through the marketplace',
'Close-up of the mysterious artifact',
'Dramatic reveal of the antagonist'
], # List must match number of images (or use single string for all)
'date': '2025-01-29',
}
```yaml
# In data/comics/005.yaml
number: 5
title: Long Scroll Episode
filename:
- scene1.png
- scene2.png
- scene3.png
- scene4.png
alt_text:
- Opening scene showing the city at dawn
- Character walking through the marketplace
- Close-up of the mysterious artifact
- Dramatic reveal of the antagonist
date: '2025-01-29'
```
**Important:** If you provide `alt_text` as a list, it should match the number of images in `filename`. If the counts don't match, you'll see a warning in the logs. To use the same alt text for all images, just provide a single string instead of a list.
@@ -866,29 +919,26 @@ Sunday Comics supports vertical scrolling comics with multiple images stacked se
4. **Image optimization** - Compress images appropriately since readers will load multiple images per comic
**Example with all options:**
```python
{
'number': 6,
'title': 'Chapter 2: The Journey Begins',
'filename': [
'ch2-001.png',
'ch2-002.png',
'ch2-003.png',
'ch2-004.png',
'ch2-005.png'
],
'alt_text': [
'Panel 1: Hero packs their bag at sunrise',
'Panel 2: Saying goodbye to the village elder',
'Panel 3: Walking along the forest path',
'Panel 4: Encountering a mysterious stranger',
'Panel 5: Accepting a map to the ancient ruins'
],
'date': '2025-02-05',
'author_note': 'This was so much fun to draw! The journey arc begins.',
'full_width': True, # Recommended for webtoon-style comics
'section': 'Chapter 2', # Optional: mark the start of a new chapter
}
```yaml
# In data/comics/006.yaml
number: 6
title: 'Chapter 2: The Journey Begins'
filename:
- ch2-001.png
- ch2-002.png
- ch2-003.png
- ch2-004.png
- ch2-005.png
alt_text:
- 'Panel 1: Hero packs their bag at sunrise'
- 'Panel 2: Saying goodbye to the village elder'
- 'Panel 3: Walking along the forest path'
- 'Panel 4: Encountering a mysterious stranger'
- 'Panel 5: Accepting a map to the ancient ruins'
date: '2025-02-05'
author_note: This was so much fun to draw! The journey arc begins.
full_width: true # Recommended for webtoon-style comics
section: Chapter 2 # Optional: mark the start of a new chapter
```
**Technical Details:**

1
VERSION Normal file
View File

@@ -0,0 +1 @@
2025.11.15

44
app.py
View File

@@ -13,6 +13,7 @@ from comics_data import (
SOCIAL_INSTAGRAM, SOCIAL_YOUTUBE, SOCIAL_EMAIL, API_SPEC_LINK, EMBED_ENABLED, PERMALINK_ENABLED
)
import markdown
from version import __version__
# Configure logging
logging.basicConfig(level=logging.WARNING)
@@ -57,7 +58,8 @@ def inject_global_settings():
'social_email': SOCIAL_EMAIL,
'api_spec_link': API_SPEC_LINK,
'embed_enabled': EMBED_ENABLED,
'permalink_enabled': PERMALINK_ENABLED
'permalink_enabled': PERMALINK_ENABLED,
'version': __version__
}
@@ -318,24 +320,19 @@ def terms():
@app.route('/api/comics')
def api_comics():
"""API endpoint - returns all comics as JSON"""
return jsonify([enrich_comic(comic) for comic in COMICS])
"""API endpoint - returns all comics as JSON (optionally paginated with sections)"""
# Check for pagination parameters
page = request.args.get('page', type=int)
per_page = request.args.get('per_page', type=int)
group_by_section = request.args.get('group_by_section', 'false').lower() in ('true', '1', 'yes')
# If no pagination requested, return simple array (backward compatible)
if page is None and per_page is None and not group_by_section:
return jsonify([enrich_comic(comic) for comic in COMICS])
@app.route('/api/comics/<int:comic_id>')
def api_comic(comic_id):
"""API endpoint - returns a specific comic as JSON"""
comic = get_comic_by_number(comic_id)
if not comic:
return jsonify({'error': 'Comic not found'}), 404
return jsonify(comic)
@app.route('/api/archive')
def api_archive():
"""API endpoint - returns paginated archive data"""
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 24, type=int)
# Pagination requested - return paginated response
page = page or 1
per_page = per_page or 24
# Limit per_page to reasonable values
per_page = min(max(per_page, 1), 100)
@@ -343,8 +340,8 @@ def api_archive():
# Reverse order to show newest first
all_comics = [enrich_comic(comic) for comic in reversed(COMICS)]
# Group by section if enabled
sections = group_comics_by_section(all_comics)
# Group by section if enabled globally or requested via parameter
sections = group_comics_by_section(all_comics) if (SECTIONS_ENABLED or group_by_section) else [(None, all_comics)]
# Calculate pagination
total_comics = len(all_comics)
@@ -388,6 +385,15 @@ def api_archive():
})
@app.route('/api/comics/<int:comic_id>')
def api_comic(comic_id):
"""API endpoint - returns a specific comic as JSON"""
comic = get_comic_by_number(comic_id)
if not comic:
return jsonify({'error': 'Comic not found'}), 404
return jsonify(comic)
@app.route('/sitemap.xml')
def sitemap():
"""Serve the static sitemap.xml file"""

154
scripts/bump_version.py Executable file
View File

@@ -0,0 +1,154 @@
#!/usr/bin/env python3
# Sunday Comics - Version bump script
# Copyright (c) 2025 Tomasita Cabrera
# Licensed under the MIT License - see LICENSE file for details
"""
Script to bump the project version number
Usage:
python scripts/bump_version.py # Use today's date
python scripts/bump_version.py 2025.12.25 # Use specific date
"""
import sys
import os
import re
from datetime import datetime
import argparse
def validate_version(version_str):
"""Validate version format (YYYY.MM.DD)"""
pattern = r'^\d{4}\.\d{2}\.\d{2}$'
if not re.match(pattern, version_str):
return False
# Try to parse as a date to ensure it's valid
try:
parts = version_str.split('.')
year, month, day = int(parts[0]), int(parts[1]), int(parts[2])
datetime(year, month, day)
return True
except ValueError:
return False
def get_current_version(parent_dir):
"""Read current version from version.py"""
version_file = os.path.join(parent_dir, 'version.py')
try:
with open(version_file, 'r', encoding='utf-8') as f:
content = f.read()
match = re.search(r'__version__\s*=\s*["\']([^"\']+)["\']', content)
if match:
return match.group(1)
except FileNotFoundError:
pass
return None
def update_version_py(parent_dir, new_version):
"""Update version.py with new version"""
version_file = os.path.join(parent_dir, 'version.py')
content = f"""# Sunday Comics Version
# This file contains the version number for the project
# Format: YYYY.MM.DD (date-based versioning)
__version__ = "{new_version}"
"""
with open(version_file, 'w', encoding='utf-8') as f:
f.write(content)
print(f"✓ Updated {version_file}")
def update_version_file(parent_dir, new_version):
"""Update VERSION file with new version"""
version_file = os.path.join(parent_dir, 'VERSION')
with open(version_file, 'w', encoding='utf-8') as f:
f.write(f"{new_version}\n")
print(f"✓ Updated {version_file}")
def remind_changelog(parent_dir, new_version, current_version):
"""Remind user to update CHANGELOG.md"""
changelog_file = os.path.join(parent_dir, 'CHANGELOG.md')
print(f"\n📝 Don't forget to update {changelog_file}!")
print(f"\nAdd your changes under the new version section:")
print(f"\n## [{new_version}] - {datetime.now().strftime('%Y-%m-%d')}")
print(f"\n### Added")
print(f"### Changed")
print(f"### Fixed")
if os.path.exists(changelog_file):
print(f"\n💡 Tip: Edit the file now with: nano {changelog_file}")
def main():
parser = argparse.ArgumentParser(
description='Bump project version number',
epilog='Examples:\n %(prog)s\n %(prog)s 2025.12.25',
formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument(
'version',
nargs='?',
help='Version number in YYYY.MM.DD format (defaults to today\'s date)'
)
parser.add_argument(
'--no-changelog-reminder',
action='store_true',
help='Skip the changelog reminder'
)
args = parser.parse_args()
# Determine new version
if args.version:
new_version = args.version
if not validate_version(new_version):
print(f"Error: Invalid version format '{new_version}'")
print(f"Expected format: YYYY.MM.DD (e.g., 2025.12.25)")
sys.exit(1)
else:
# Use today's date
new_version = datetime.now().strftime('%Y.%m.%d')
# Get parent directory (project root)
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Get current version
current_version = get_current_version(parent_dir)
if current_version:
print(f"Current version: {current_version}")
print(f"New version: {new_version}")
# Check if version is the same
if current_version == new_version:
print(f"\n⚠️ Version is already {new_version}")
response = input("Continue anyway? [y/N]: ").lower().strip()
if response != 'y':
print("Aborted.")
sys.exit(0)
# Update files
print(f"\nUpdating version files...")
update_version_py(parent_dir, new_version)
update_version_file(parent_dir, new_version)
print(f"\n✅ Version bumped to {new_version}")
# Remind about changelog
if not args.no_changelog_reminder:
remind_changelog(parent_dir, new_version, current_version)
if __name__ == '__main__':
main()

View File

@@ -41,7 +41,7 @@
try {
currentPage++;
const response = await fetch(`/api/archive?page=${currentPage}&per_page=${perPage}`);
const response = await fetch(`/api/comics?page=${currentPage}&per_page=${perPage}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);

View File

@@ -16,39 +16,107 @@ paths:
/api/comics:
get:
summary: Get all comics
description: Returns a list of all comics with enriched metadata including formatted dates and author notes
description: |
Returns all comics with enriched metadata. Supports optional pagination and section grouping.
**Without pagination parameters:** Returns a simple array of all comics (newest first when using pagination, original order otherwise).
**With pagination parameters:** Returns paginated response with section grouping (if enabled globally or via `group_by_section` parameter).
operationId: getAllComics
tags:
- Comics
parameters:
- name: page
in: query
description: Page number for pagination (1-indexed). When provided, triggers paginated response format.
required: false
schema:
type: integer
minimum: 1
default: 1
example: 1
- name: per_page
in: query
description: Number of comics per page (max 100). When provided, triggers paginated response format.
required: false
schema:
type: integer
minimum: 1
maximum: 100
default: 24
example: 24
- name: group_by_section
in: query
description: Force section grouping in response (even when SECTIONS_ENABLED is false). When true, triggers paginated response format.
required: false
schema:
type: boolean
default: false
example: false
responses:
'200':
description: Successful response with array of comics
description: Successful response
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Comic'
example:
- number: 1
title: "First Comic"
filename: "comic-001.jpg"
mobile_filename: "comic-001-mobile.jpg"
date: "2025-01-01"
alt_text: "The very first comic"
author_note: "This is where your comic journey begins!"
full_width: true
plain: true
formatted_date: "Wednesday, January 1, 2025"
author_note_is_html: false
- number: 2
filename: "comic-002.jpg"
date: "2025-01-08"
alt_text: "The second comic"
full_width: true
plain: true
formatted_date: "Wednesday, January 8, 2025"
author_note_is_html: false
oneOf:
- type: array
description: Simple array response (when no pagination parameters provided)
items:
$ref: '#/components/schemas/Comic'
- $ref: '#/components/schemas/PaginatedComicsResponse'
examples:
simpleArray:
summary: Simple array response (default)
value:
- number: 1
title: "First Comic"
filename: "comic-001.jpg"
mobile_filename: "comic-001-mobile.jpg"
date: "2025-01-01"
alt_text: "The very first comic"
author_note: "This is where your comic journey begins!"
full_width: true
plain: true
formatted_date: "Wednesday, January 1, 2025"
author_note_is_html: false
- number: 2
filename: "comic-002.jpg"
date: "2025-01-08"
alt_text: "The second comic"
full_width: true
plain: true
formatted_date: "Wednesday, January 8, 2025"
author_note_is_html: false
paginatedResponse:
summary: Paginated response (when using page/per_page parameters)
value:
sections:
- section_title: "Chapter 1"
comics:
- number: 2
filename: "comic-002.jpg"
date: "2025-01-08"
alt_text: "The second comic"
full_width: true
plain: true
formatted_date: "Wednesday, January 8, 2025"
author_note_is_html: false
- section_title: null
comics:
- number: 1
title: "First Comic"
filename: "comic-001.jpg"
date: "2025-01-01"
alt_text: "The very first comic"
full_width: true
plain: true
formatted_date: "Wednesday, January 1, 2025"
author_note_is_html: false
page: 1
per_page: 24
total_comics: 2
has_more: false
/api/comics/{comic_id}:
get:
@@ -107,6 +175,9 @@ components:
- plain
- formatted_date
- author_note_is_html
- filenames
- alt_texts
- is_multi_image
properties:
number:
type: integer
@@ -118,9 +189,20 @@ components:
description: Comic title (optional, defaults to "#X" if not provided)
example: "First Comic"
filename:
type: string
description: Image filename in static/images/comics/
oneOf:
- type: string
description: Single image filename in static/images/comics/
- type: array
description: Multiple image filenames for webtoon-style comics
items:
type: string
example: "comic-001.jpg"
filenames:
type: array
description: Normalized array of image filenames (computed field, always an array even for single images)
items:
type: string
example: ["comic-001.jpg"]
mobile_filename:
type: string
description: Optional mobile version of the comic image
@@ -131,13 +213,36 @@ components:
description: Publication date in YYYY-MM-DD format
example: "2025-01-01"
alt_text:
type: string
description: Accessibility text for the comic image
oneOf:
- type: string
description: Accessibility text for single image or shared text for all images
- type: array
description: Individual accessibility text for each image in multi-image comics
items:
type: string
example: "The very first comic"
alt_texts:
type: array
description: Normalized array of alt texts matching filenames (computed field)
items:
type: string
example: ["The very first comic"]
is_multi_image:
type: boolean
description: Indicates if this is a multi-image comic (computed field)
example: false
author_note:
type: string
description: Author's note about the comic (plain text or HTML from markdown)
example: "This is where your comic journey begins!"
author_note_md:
type: string
description: Filename or path to markdown file for author note
example: "2025-01-01.md"
section:
type: string
description: Section/chapter title (appears on archive page when SECTIONS_ENABLED is true)
example: "Chapter 1: Origins"
full_width:
type: boolean
description: Whether the comic should display in full-width mode (computed from global default and per-comic override)
@@ -155,6 +260,58 @@ components:
description: Indicates whether author_note contains HTML (from markdown) or plain text (computed field)
example: false
ComicSection:
type: object
required:
- section_title
- comics
properties:
section_title:
type: string
nullable: true
description: Section/chapter title (null for comics without a section)
example: "Chapter 1"
comics:
type: array
description: Comics in this section
items:
$ref: '#/components/schemas/Comic'
PaginatedComicsResponse:
type: object
required:
- sections
- page
- per_page
- total_comics
- has_more
properties:
sections:
type: array
description: Comics grouped by section
items:
$ref: '#/components/schemas/ComicSection'
page:
type: integer
description: Current page number
minimum: 1
example: 1
per_page:
type: integer
description: Number of comics per page
minimum: 1
maximum: 100
example: 24
total_comics:
type: integer
description: Total number of comics across all pages
minimum: 0
example: 100
has_more:
type: boolean
description: Whether there are more pages available
example: true
Error:
type: object
required:

View File

@@ -9,6 +9,9 @@
<meta name="description" content="{% block meta_description %}A webcomic about life, the universe, and everything{% endblock %}">
<link rel="canonical" href="{% block canonical %}{{ site_url }}{{ request.path }}{% endblock %}">
<!-- Version -->
<meta name="generator" content="Sunday Comics {{ version }}">
<!-- AI Scraping Prevention -->
<meta name="robots" content="noai, noimageai">
<meta name="googlebot" content="noai, noimageai">

5
version.py Normal file
View File

@@ -0,0 +1,5 @@
# Sunday Comics Version
# This file contains the version number for the project
# Format: YYYY.MM.DD (date-based versioning)
__version__ = "2025.11.15"