Files
sunday/CLAUDE.md
2025-11-18 13:40:52 +10:00

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

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 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)
  • 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.

HTML embeds in 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:

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

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 (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
  • 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_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
  • 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 (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