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 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):
- Set
filenameto a list of image filenames:['page1.png', 'page2.png', 'page3.png'] - Set
alt_textto either:- A single string (applies to all images):
'A three-part vertical story' - A list matching each image:
['Description 1', 'Description 2', 'Description 3']
- A single string (applies to all images):
- 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 footerSOCIAL_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.pysocial_instagram: Instagram URL fromcomics_data.pysocial_youtube: YouTube URL fromcomics_data.pysocial_email: Email link fromcomics_data.py
Static Assets
static/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 ordering: COMICS list order 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. -
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.
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 (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-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