21 KiB
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 logicdata_loader.py- YAML loading and cachingtemplates/*.html- Jinja2 templatesstatic/css/style.css- Core framework styles (references CSS variables)static/js/*.js- Client-side navigation and functionalityscripts/*.py- Utility scriptsversion.py,VERSION- Version managementDockerfile,docker-compose.yml- Deployment configuration
User Customization Files (Safe for users to modify):
comics_data.py- Global configuration settingsstatic/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 optionsdata/comics/TEMPLATE.yaml- Comic file template
Generated Files (Auto-created, gitignored):
static/feed.rss,static/sitemap.xml- Generated by scriptsdata/comics/.comics_cache.pkl- Comic cache__pycache__/,*.pyc- Python bytecode
CSS Architecture
Two-file CSS system to separate customization from framework:
-
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
-
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.cssthat 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:
- Add the option to
comics_data.pywith a default value - Add the same option to
comics_data.py.examplewith documentation - Update
CHANGELOG.mdwith migration instructions - 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:
- Ask: "Will users want to customize this?"
- If yes: Add a variable/config option
- If no: Implement in framework code
- Always maintain the separation
Upstream Update Workflow
Users following UPSTREAM.md will:
- Fork the repository
- Customize
comics_data.pyandvariables.css - Add their comics and content
- Periodically merge upstream updates:
git merge upstream/main - Resolve conflicts (usually only in .example files)
- Benefit from framework improvements without losing customizations
Development Commands
Run the development server:
python app.py
Server runs on http://127.0.0.1:3000 by default.
Enable debug mode:
export DEBUG=True
python app.py
Add a new comic:
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:
python scripts/add_comic.py -m
This also creates a markdown file in content/author_notes/ for the author note.
Generate RSS feed:
python scripts/generate_rss.py
Run this after adding/updating comics to regenerate static/feed.rss.
Generate sitemap:
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):
python scripts/publish_comic.py
Convenience script that rebuilds the cache and regenerates all static files in one command.
Rebuild comics cache:
python scripts/rebuild_cache.py
Force rebuild the comics cache from YAML files. Normally not needed (cache auto-invalidates).
Bump version:
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:
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 withfrom version import __version__)VERSION: Plain text file at project root for easy access by scripts and CI/CDCHANGELOG.md: Tracks version history and changes following Keep a Changelog format- HTML meta tag: Version appears in page source as
<meta name="generator" content="Sunday Comics X.Y.Z">
When releasing a new version:
- Run
python scripts/bump_version.pyto update version files - Edit
CHANGELOG.mdto document changes under the new version - Commit changes:
git commit -m "Release version YYYY.MM.DD" - Tag the release:
git tag -a vYYYY.MM.DD -m "Version YYYY.MM.DD" - 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 #1data/comics/002.yaml- Comic #2data/comics/003.yaml- Comic #3data/comics/TEMPLATE.yaml- Template for new comics (ignored by loader)data/comics/README.md- Documentation for comic files
Adding a new comic:
- Use
python scripts/add_comic.pyto auto-generate the next comic file - OR manually copy
TEMPLATE.yamland rename it - Edit the YAML file to set comic properties
Each comic YAML file contains:
number(required): Sequential comic numberfilename(required): Image filename instatic/images/comics/OR list of filenames for multi-image comics (webtoon style)date(required): Publication date in YYYY-MM-DD formatalt_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 noteauthor_note_md(optional): Markdown file for author note (just filename like "2025-01-01.md" looks incontent/author_notes/, or path like "special/note.md" relative tocontent/). Takes precedence overauthor_note.full_width(optional): Override global FULL_WIDTH_DEFAULT settingplain(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:
filename:
- page1.png
- page2.png
- page3.png
alt_text:
- "Description 1"
- "Description 2"
- "Description 3"
- Set
filenameto a list of image filenames - Set
alt_textto either a single string (applies to all images) or a list matching each image - If
alt_textis a list but doesn't matchfilenamelength, 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 nameCOPYRIGHT_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 defaultPLAIN_DEFAULT: Set to True to hide header/remove borders by defaultARCHIVE_FULL_WIDTH: Set to True to make archive page full-width with 4 columnsSECTIONS_ENABLED: Set to True to enable section headers on archive page (usessectionfield from comics)HEADER_IMAGE: Path relative tostatic/images/for site header image (set to None to disable)FOOTER_IMAGE: Path relative tostatic/images/for site footer image (set to None to disable)BANNER_IMAGE: Path relative tostatic/images/for shareable banner (set to None to hide "Link to Us" section)COMPACT_FOOTER: Display footer in compact single-line modeUSE_COMIC_NAV_ICONS: Set to True to use icon images for comic navigation buttons instead of text (requires icons instatic/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 footerNEWSLETTER_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 numberget_latest_comic(): Returns the last comic in the COMICS listformat_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 incontent/author_notes/, paths are relative tocontent/.group_comics_by_section(comics_list): Groups comics by section based onsectionfield. 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 buttonsarchive.html: Grid layout with thumbnails fromstatic/images/thumbs/page.html: Generic template for markdown content (used by /about)
Global context variables injected into all templates:
comic_name: Site/comic name fromcomics_data.pycopyright_name: Copyright name fromcomics_data.py(defaults tocomic_nameif not set)current_year: Current year (auto-generated from system time)header_image: Site header image path fromcomics_data.pyfooter_image: Site footer image path fromcomics_data.pybanner_image: Shareable banner image path fromcomics_data.pycompact_footer: Boolean for footer style fromcomics_data.pyuse_comic_nav_icons: Boolean for comic navigation icons fromcomics_data.pyuse_header_nav_icons: Boolean for main header navigation icons fromcomics_data.pyuse_footer_social_icons: Boolean for footer social link icons fromcomics_data.pynewsletter_enabled: Boolean to show/hide newsletter section fromcomics_data.pynewsletter_html: Custom HTML for newsletter form fromcomics_data.py(rendered with| safefilter)social_instagram: Instagram URL fromcomics_data.pysocial_youtube: YouTube URL fromcomics_data.pysocial_email: Email link fromcomics_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 navigationstatic/images/comics/: Full-size comic imagesstatic/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 whenUSE_ICON_NAVis Truestatic/images/: Header images and other site graphicsstatic/feed.rss: Generated RSS feed (runscripts/generate_rss.py)static/sitemap.xml: Generated sitemap (runscripts/generate_sitemap.py)
Important Implementation Details
-
Comic loading: The
data_loader.pymodule scansdata/comics/for.yamlfiles, loads them, validates required fields, and sorts by comic number. TEMPLATE.yaml and README.yaml are automatically ignored. Results are cached to.comics_cache.pklfor performance. -
Comic ordering: COMICS list order (determined by the
numberfield in each YAML file) determines comic sequence. Last item is the "latest" comic. -
Enrichment pattern: Always use
enrich_comic()before passing comics to templates or APIs. This adds computed properties likefull_width,plain, andformatted_date. -
Date formatting: The
format_comic_date()function uses%dwith lstrip('0') for cross-platform compatibility (not all systems support%-d). -
Author notes hierarchy: If
author_note_mdfield is specified, the markdown file is loaded and rendered as HTML, taking precedence over the plain textauthor_notefield. When markdown is used,author_note_is_htmlis set to True. -
Settings cascade: Global settings (FULL_WIDTH_DEFAULT, PLAIN_DEFAULT) apply unless overridden per-comic with
full_widthorplainkeys in the YAML file. -
Navigation state: Client-side navigation reads
data-total-comicsanddata-comic-numberfrom the.comic-containerelement to manage button states. -
Comic icon navigation: When
USE_COMIC_NAV_ICONSis True, templates use.btn-icon-navclass with icon images instead of text buttons. JavaScript automatically detects icon mode and applies appropriate classes. Disabled icons have reduced opacity (0.3). -
Archive sections: When
SECTIONS_ENABLEDis True, comics with asectionfield will start a new section on the archive page. Only add thesectionfield to the first comic of each new section. All subsequent comics belong to that section until a newsectionfield is encountered. -
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
docker-compose up -d
Alternative: Gunicorn
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 withsecrets.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 instatic/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-focuselement
Screen Reader Support
- ARIA live region:
#comic-announcerelement announces comic changes witharia-live="polite" - ARIA labels: All icon buttons have descriptive
aria-labelattributes - 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-onlyclass: Hides content visually but keeps it accessible to screen readers.skip-to-mainclass: Positioned off-screen until focused, then slides into view- Focus styles use
outlineproperty (never remove without replacement) - Disabled buttons use both color and opacity for visibility
JavaScript Accessibility
The comic-nav.js file handles:
- Announcements: Updates
#comic-announcertext content on navigation - Focus: Calls
.focus()on#comic-image-focusafter loading comic - ARIA attributes: Dynamically adds/removes
aria-disabledon navigation buttons - 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