Files
sunday/CLAUDE.md
2025-11-15 18:52:14 +10:00

13 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 simple Python dictionaries in comics_data.py, making the system easy to manage without a database.

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 entry in comics_data.py with defaults. Edit the file afterwards to customize title, alt_text, author_note, etc.

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.

Architecture

Data Layer: comics_data.py

Comics are stored as a Python list called COMICS. Each comic is a dictionary with:

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

  • Set filename to a list of image filenames: ['page1.png', 'page2.png', 'page3.png']
  • Set alt_text to either:
    • A single string (applies to all images): 'A three-part vertical story'
    • A list matching each image: ['Description 1', 'Description 2', 'Description 3']
  • 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 ordering: COMICS list order determines comic sequence. Last item is the "latest" comic.

  2. Enrichment pattern: Always use enrich_comic() before passing comics to templates or APIs. This adds computed properties like full_width, plain, and formatted_date.

  3. Date formatting: The format_comic_date() function uses %d with lstrip('0') for cross-platform compatibility (not all systems support %-d).

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

  5. Settings cascade: Global settings (FULL_WIDTH_DEFAULT, PLAIN_DEFAULT) apply unless overridden per-comic with full_width or plain keys.

  6. Navigation state: Client-side navigation reads data-total-comics and data-comic-number from the .comic-container element to manage button states.

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

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

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