436 lines
21 KiB
Markdown
436 lines
21 KiB
Markdown
# CLAUDE.md
|
|
|
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
|
|
## Project Overview
|
|
|
|
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:**
|
|
```bash
|
|
python app.py
|
|
```
|
|
Server runs on http://127.0.0.1:3000 by default.
|
|
|
|
**Enable debug mode:**
|
|
```bash
|
|
export DEBUG=True
|
|
python app.py
|
|
```
|
|
|
|
**Add a new comic:**
|
|
```bash
|
|
python scripts/add_comic.py
|
|
```
|
|
This creates a new YAML file in `data/comics/` with the next comic number and reasonable defaults. Edit the generated file to customize title, alt_text, author_note, etc.
|
|
|
|
**Add a new comic with markdown author note:**
|
|
```bash
|
|
python scripts/add_comic.py -m
|
|
```
|
|
This also creates a markdown file in `content/author_notes/` for the author note.
|
|
|
|
**Generate RSS feed:**
|
|
```bash
|
|
python scripts/generate_rss.py
|
|
```
|
|
Run this after adding/updating comics to regenerate `static/feed.rss`.
|
|
|
|
**Generate sitemap:**
|
|
```bash
|
|
python scripts/generate_sitemap.py
|
|
```
|
|
Run this after adding/updating comics to regenerate `static/sitemap.xml` for search engines.
|
|
|
|
**Publish comics (rebuild cache + RSS + sitemap):**
|
|
```bash
|
|
python scripts/publish_comic.py
|
|
```
|
|
Convenience script that rebuilds the cache and regenerates all static files in one command.
|
|
|
|
**Rebuild comics cache:**
|
|
```bash
|
|
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/
|
|
|
|
Comics are stored as individual YAML files in the `data/comics/` directory. The `data_loader.py` module automatically loads all `.yaml` files (except `TEMPLATE.yaml` and `README.yaml`), sorts them by comic number, and builds the `COMICS` list.
|
|
|
|
**Caching:** The data loader uses automatic caching to speed up subsequent loads:
|
|
- 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
|
|
- Cache can be disabled via environment variable: `DISABLE_COMIC_CACHE=true`
|
|
|
|
Performance with caching (1000 comics):
|
|
- Initial load: ~2-3 seconds (builds cache)
|
|
- Subsequent loads: ~0.01 seconds (uses cache)
|
|
- Scripts (RSS, sitemap): All share the same cache file on disk
|
|
|
|
**File structure:**
|
|
- `data/comics/001.yaml` - Comic #1
|
|
- `data/comics/002.yaml` - Comic #2
|
|
- `data/comics/003.yaml` - Comic #3
|
|
- `data/comics/TEMPLATE.yaml` - Template for new comics (ignored by loader)
|
|
- `data/comics/README.md` - Documentation for comic files
|
|
|
|
**Adding a new comic:**
|
|
1. Use `python scripts/add_comic.py` to auto-generate the next comic file
|
|
2. OR manually copy `TEMPLATE.yaml` and rename it
|
|
3. Edit the YAML file to set comic properties
|
|
|
|
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)
|
|
- `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)
|
|
- `author_note` (optional): Plain text note
|
|
- `author_note_md` (optional): Markdown file for author note (just filename like "2025-01-01.md" looks in `content/author_notes/`, or path like "special/note.md" relative to `content/`). Takes precedence over `author_note`.
|
|
- `full_width` (optional): Override global FULL_WIDTH_DEFAULT setting
|
|
- `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.
|
|
|
|
**Multi-image comics (webtoon style) in YAML:**
|
|
```yaml
|
|
filename:
|
|
- page1.png
|
|
- page2.png
|
|
- page3.png
|
|
alt_text:
|
|
- "Description 1"
|
|
- "Description 2"
|
|
- "Description 3"
|
|
```
|
|
- Set `filename` to a list of image filenames
|
|
- Set `alt_text` to either a single string (applies to all images) or a list matching each image
|
|
- If `alt_text` is a list but doesn't match `filename` length, a warning is logged
|
|
- Images display vertically with seamless stacking (no gaps)
|
|
- First image loads immediately; subsequent images lazy-load as user scrolls
|
|
- No click-through navigation on multi-image comics (use navigation buttons instead)
|
|
|
|
Global configuration in `comics_data.py`:
|
|
- `COMIC_NAME`: Your comic/website name
|
|
- `COPYRIGHT_NAME`: Name to display in copyright notice (defaults to COMIC_NAME if not set)
|
|
- `FULL_WIDTH_DEFAULT`: Set to True to make all comics full-width by default
|
|
- `PLAIN_DEFAULT`: Set to True to hide header/remove borders by default
|
|
- `ARCHIVE_FULL_WIDTH`: Set to True to make archive page full-width with 4 columns
|
|
- `SECTIONS_ENABLED`: Set to True to enable section headers on archive page (uses `section` field from comics)
|
|
- `HEADER_IMAGE`: Path relative to `static/images/` for site header image (set to None to disable)
|
|
- `FOOTER_IMAGE`: Path relative to `static/images/` for site footer image (set to None to disable)
|
|
- `BANNER_IMAGE`: Path relative to `static/images/` for shareable banner (set to None to hide "Link to Us" section)
|
|
- `COMPACT_FOOTER`: Display footer in compact single-line mode
|
|
- `USE_COMIC_NAV_ICONS`: Set to True to use icon images for comic navigation buttons instead of text (requires icons in `static/images/icons/`)
|
|
- `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
|
|
- `NEWSLETTER_HTML`: Custom HTML for newsletter form (user pastes their service's form code here)
|
|
- `SOCIAL_INSTAGRAM`: Instagram URL (set to None to hide)
|
|
- `SOCIAL_YOUTUBE`: YouTube URL (set to None to hide)
|
|
- `SOCIAL_EMAIL`: Email mailto link (set to None to hide)
|
|
|
|
### Markdown Support
|
|
|
|
**Author Notes:** Add `author_note_md` field to comic entries with a filename (e.g., `"2025-01-01.md"`) or path relative to `content/` (e.g., `"special/note.md"`). Just a filename looks in `content/author_notes/`. The markdown content is rendered as HTML and takes precedence over the plain text `author_note` field.
|
|
|
|
**About Page:** The `/about` route renders `content/about.md` as HTML using the markdown library.
|
|
|
|
### Flask Application: app.py
|
|
|
|
**Core Functions:**
|
|
- `enrich_comic(comic)`: Adds computed properties (full_width, plain, formatted_date, author_note from markdown)
|
|
- `get_comic_by_number(number)`: Retrieves and enriches a comic by number
|
|
- `get_latest_comic()`: Returns the last comic in the COMICS list
|
|
- `format_comic_date(date_str)`: Converts YYYY-MM-DD to "Day name, Month name day, year"
|
|
- `get_author_note_from_file(filename)`: Loads markdown author note from file. Filename alone looks in `content/author_notes/`, paths are relative to `content/`.
|
|
- `group_comics_by_section(comics_list)`: Groups comics by section based on `section` field. Returns list of (section_title, comics) tuples.
|
|
|
|
**Routes:**
|
|
- `/` - Latest comic (index.html)
|
|
- `/comic/<id>` - Specific comic viewer (comic.html)
|
|
- `/archive` - Grid of all comics, newest first (archive.html)
|
|
- `/about` - Renders markdown from content/about.md (page.html)
|
|
- `/api/comics` - JSON array of all comics
|
|
- `/api/comics/<id>` - JSON for a specific comic
|
|
|
|
### Client-Side Navigation: static/js/comic-nav.js
|
|
|
|
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
|
|
- Uses History API to maintain proper URLs and browser back/forward
|
|
- Shows/hides header based on plain mode
|
|
- Adjusts container for full_width mode
|
|
- Updates author notes dynamically (plain text only; markdown rendering requires page reload)
|
|
|
|
**Note:** Client-side navigation displays author notes as plain text. Markdown author notes only render properly on initial page load (server-side).
|
|
|
|
### Templates
|
|
|
|
Built with Jinja2, extending `base.html`:
|
|
- `base.html`: Contains navigation header, footer, metadata tags (Open Graph, Twitter Cards)
|
|
- `index.html` & `comic.html`: Display comics with navigation buttons
|
|
- `archive.html`: Grid layout with thumbnails from `static/images/thumbs/`
|
|
- `page.html`: Generic template for markdown content (used by /about)
|
|
|
|
Global context variables injected into all templates:
|
|
- `comic_name`: Site/comic name from `comics_data.py`
|
|
- `copyright_name`: Copyright name from `comics_data.py` (defaults to `comic_name` if not set)
|
|
- `current_year`: Current year (auto-generated from system time)
|
|
- `header_image`: Site header image path from `comics_data.py`
|
|
- `footer_image`: Site footer image path from `comics_data.py`
|
|
- `banner_image`: Shareable banner image path from `comics_data.py`
|
|
- `compact_footer`: Boolean for footer style from `comics_data.py`
|
|
- `use_comic_nav_icons`: Boolean for comic navigation icons from `comics_data.py`
|
|
- `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`
|
|
- `newsletter_html`: Custom HTML for newsletter form from `comics_data.py` (rendered with `| safe` filter)
|
|
- `social_instagram`: Instagram URL from `comics_data.py`
|
|
- `social_youtube`: YouTube URL from `comics_data.py`
|
|
- `social_email`: Email link from `comics_data.py`
|
|
|
|
## 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
|
|
- `static/images/`: Header images and other site graphics
|
|
- `static/feed.rss`: Generated RSS feed (run `scripts/generate_rss.py`)
|
|
- `static/sitemap.xml`: Generated sitemap (run `scripts/generate_sitemap.py`)
|
|
|
|
## Important Implementation Details
|
|
|
|
1. **Comic loading**: The `data_loader.py` module scans `data/comics/` for `.yaml` files, loads them, validates required fields, and sorts by comic number. TEMPLATE.yaml and README.yaml are automatically ignored. Results are cached to `.comics_cache.pkl` for performance.
|
|
|
|
2. **Comic ordering**: COMICS list order (determined by the `number` field in each YAML file) determines comic sequence. Last item is the "latest" comic.
|
|
|
|
3. **Enrichment pattern**: Always use `enrich_comic()` before passing comics to templates or APIs. This adds computed properties like `full_width`, `plain`, and `formatted_date`.
|
|
|
|
4. **Date formatting**: The `format_comic_date()` function uses `%d` with lstrip('0') for cross-platform compatibility (not all systems support `%-d`).
|
|
|
|
5. **Author notes hierarchy**: If `author_note_md` field is specified, the markdown file is loaded and rendered as HTML, taking precedence over the plain text `author_note` field. When markdown is used, `author_note_is_html` is set to True.
|
|
|
|
6. **Settings cascade**: Global settings (FULL_WIDTH_DEFAULT, PLAIN_DEFAULT) apply unless overridden per-comic with `full_width` or `plain` keys in the YAML file.
|
|
|
|
7. **Navigation state**: Client-side navigation reads `data-total-comics` and `data-comic-number` from the `.comic-container` element to manage button states.
|
|
|
|
8. **Comic icon navigation**: When `USE_COMIC_NAV_ICONS` is True, templates use `.btn-icon-nav` class with icon images instead of text buttons. JavaScript automatically detects icon mode and applies appropriate classes. Disabled icons have reduced opacity (0.3).
|
|
|
|
9. **Archive sections**: When `SECTIONS_ENABLED` is True, comics with a `section` field will start a new section on the archive page. Only add the `section` field to the first comic of each new section. All subsequent comics belong to that section until a new `section` field is encountered.
|
|
|
|
10. **YAML validation**: The data loader validates each comic file and logs warnings for missing required fields (`number`, `filename`, `date`, `alt_text`). Invalid files are skipped.
|
|
|
|
## Production Deployment
|
|
|
|
The app uses Flask's development server by default. For production:
|
|
|
|
**Recommended: Docker**
|
|
```bash
|
|
docker-compose up -d
|
|
```
|
|
|
|
**Alternative: Gunicorn**
|
|
```bash
|
|
pip install gunicorn
|
|
export SECRET_KEY="$(python -c 'import secrets; print(secrets.token_hex(32))')"
|
|
export DEBUG=False
|
|
gunicorn app:app --bind 0.0.0.0:3000 --workers 4
|
|
```
|
|
|
|
Environment variables:
|
|
- `SECRET_KEY`: Flask secret key (generate with `secrets.token_hex(32)`)
|
|
- `PORT`: Server port (default: 3000)
|
|
- `DEBUG`: Debug mode (default: False)
|
|
|
|
## Accessibility Implementation
|
|
|
|
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 (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
|
|
|
|
### Screen Reader Support
|
|
- **ARIA live region**: `#comic-announcer` element announces comic changes with `aria-live="polite"`
|
|
- **ARIA labels**: All icon buttons have descriptive `aria-label` attributes
|
|
- **ARIA disabled**: Disabled navigation buttons include `aria-disabled="true"`
|
|
- **Empty alt text**: Decorative icons (next to text labels) use `alt=""` to prevent redundant announcements
|
|
- **Clickable images**: Links wrapping comic images have `aria-label="Click to view next comic"`
|
|
|
|
### Semantic HTML
|
|
- Proper heading hierarchy (h1 → h2 → h3)
|
|
- `lang="en"` on html element
|
|
- Semantic elements: `<header>`, `<nav>`, `<main>`, `<footer>`
|
|
- `tabindex="-1"` on programmatically focusable elements (not in tab order)
|
|
|
|
### CSS Accessibility
|
|
- `.sr-only` class: Hides content visually but keeps it accessible to screen readers
|
|
- `.skip-to-main` class: Positioned off-screen until focused, then slides into view
|
|
- Focus styles use `outline` property (never remove without replacement)
|
|
- Disabled buttons use both color and opacity for visibility
|
|
|
|
### JavaScript Accessibility
|
|
The `comic-nav.js` file handles:
|
|
1. **Announcements**: Updates `#comic-announcer` text content on navigation
|
|
2. **Focus**: Calls `.focus()` on `#comic-image-focus` after loading comic
|
|
3. **ARIA attributes**: Dynamically adds/removes `aria-disabled` on navigation buttons
|
|
4. **Boundary feedback**: Announces "Already at first/latest comic" at navigation limits
|
|
|
|
### Maintaining Accessibility
|
|
When adding features:
|
|
- Ensure all images have meaningful alt text
|
|
- Test with keyboard only (no mouse)
|
|
- Verify focus indicators are visible
|
|
- Check ARIA labels on icon buttons
|
|
- Test with screen readers when possible (VoiceOver, NVDA)
|
|
- Maintain semantic HTML structure
|
|
|
|
## Testing Approach
|
|
|
|
No test suite currently exists. When adding tests, consider:
|
|
- Comic retrieval and enrichment logic
|
|
- API endpoint responses
|
|
- Date formatting edge cases
|
|
- Markdown rendering for author notes and about page
|
|
- Accessibility: keyboard navigation, ARIA attributes, focus management
|