Files
sunday/CLAUDE.md
2025-11-15 20:53:31 +10:00

16 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.

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 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 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:

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
  • 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
  • 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/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

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 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 (defined 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