Files
pi-weekly-newspaper/docs/superpowers/plans/2026-04-06-cover-refinements.md
cottongin d6cef67420 Add cover refinements implementation plan
Five-task plan covering pixel-width truncation, font size bump,
and obituary filtering across three call sites.

Made-with: Cursor
2026-04-06 18:56:38 -04:00

8.9 KiB

Cover Refinements Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Improve cover headline rendering by filtering out obituaries, increasing font size, and using pixel-width truncation.

Architecture: Three isolated changes that converge on the cover: (1) a new _truncate_to_width helper in cover.py replaces character-count truncation with Pillow textbbox measurement, combined with font/spacing bumps; (2) the three call sites that build headline lists gain an obituary filter; (3) tests validate both behaviors.

Tech Stack: Python, Pillow (PIL), Flask, SQLAlchemy


Task 1: Add pixel-width truncation helper and update font/spacing in cover.py

Files:

  • Modify: src/cover.py:73-114 (the _draw_text_overlays function)

  • Test: tests/test_cover.py

  • Step 1: Write failing test for pixel-width truncation

Add a new test to tests/test_cover.py that verifies headlines are truncated based on pixel width, not character count. Import _draw_text_overlays and the helper.

# Add to imports at top of tests/test_cover.py:
from PIL import ImageDraw, ImageFont
from src.cover import _truncate_to_width, _get_font

# Add this test:
def test_truncate_to_width_respects_pixel_budget():
    font = _get_font(18)
    short = "Short text"
    result = _truncate_to_width(short, font, 400)
    assert result == short, "Short text should not be truncated"

    long = "A" * 200
    result = _truncate_to_width(long, font, 400)
    assert result.endswith("\u2026"), "Long text should end with ellipsis"
    assert len(result) < len(long), "Truncated text should be shorter"


def test_truncate_to_width_fits_within_budget():
    font = _get_font(18)
    long = "This is a really long headline that should definitely be truncated to fit"
    max_width = 448
    result = _truncate_to_width(long, font, max_width)

    dummy = PILImage.new("RGBA", (1, 1))
    draw = ImageDraw.Draw(dummy)
    bbox = draw.textbbox((0, 0), result, font=font)
    rendered_width = bbox[2] - bbox[0]
    assert rendered_width <= max_width, f"Rendered width {rendered_width} exceeds {max_width}"
  • Step 2: Run tests to verify they fail

Run: python -m pytest tests/test_cover.py::test_truncate_to_width_respects_pixel_budget tests/test_cover.py::test_truncate_to_width_fits_within_budget -v Expected: FAIL with ImportError (cannot import _truncate_to_width)

  • Step 3: Implement _truncate_to_width and update _draw_text_overlays

In src/cover.py, add the new helper function after _get_dominant_category (after line 70):

def _truncate_to_width(
    text: str, font: ImageFont.FreeTypeFont, max_width: int
) -> str:
    dummy = PILImage.new("RGBA", (1, 1))
    draw = ImageDraw.Draw(dummy)
    bbox = draw.textbbox((0, 0), text, font=font)
    if (bbox[2] - bbox[0]) <= max_width:
        return text
    ellipsis = "\u2026"
    for i in range(len(text) - 1, 0, -1):
        candidate = text[:i] + ellipsis
        bbox = draw.textbbox((0, 0), candidate, font=font)
        if (bbox[2] - bbox[0]) <= max_width:
            return candidate
    return ellipsis

Then update _draw_text_overlays — change line 83 and lines 101-114:

    headline_font = _get_font(18)

And replace the headline strip block:

    if headlines:
        max_headlines = min(len(headlines), 5)
        strip_height = 28 + max_headlines * 26
        strip_top = height - strip_height
        draw.rectangle(
            [(0, strip_top), (width, height)], fill=(0, 0, 0, 170)
        )
        max_width = width - 32
        y = strip_top + 8
        for headline in headlines[:max_headlines]:
            text = f"\u2022 {headline}"
            text = _truncate_to_width(text, headline_font, max_width)
            draw.text((16, y), text, fill="white", font=headline_font)
            y += 26
  • Step 4: Run tests to verify they pass

Run: python -m pytest tests/test_cover.py -v Expected: ALL PASS (including the existing tests which verify image size/format)

  • Step 5: Commit
git add src/cover.py tests/test_cover.py
git commit -m "feat(cover): pixel-width headline truncation and larger font

Replace character-count truncation (45 chars) with Pillow textbbox
pixel-width measurement. Bump headline font 14→18, line spacing 22→26."

Task 2: Filter obituaries from cover headlines in publish.py

Files:

  • Modify: src/routes/publish.py:162-169

  • Step 1: Update headline and category list building to filter obituaries

In src/routes/publish.py, replace lines 162-169 in the create_issue function:

    articles_for_issue = (
        Article.query.filter(Article.id.in_(included_ids))
        .order_by(Article.pub_date.asc())
        .all()
    )
    headlines = [
        a.title for a in articles_for_issue
        if "Obituaries" not in json.loads(a.categories)
    ]
    categories_list = []
    for a in articles_for_issue:
        categories_list.extend(json.loads(a.categories))

This queries articles once (instead of twice), filters obituaries from headlines only, and keeps all categories for AI prompt selection.

  • Step 2: Run existing tests

Run: python -m pytest -v Expected: ALL PASS

  • Step 3: Commit
git add src/routes/publish.py
git commit -m "feat(cover): filter obituaries from headlines in manual publish"

Task 3: Filter obituaries from cover headlines in issues.py

Files:

  • Modify: src/routes/issues.py:106-113

  • Step 1: Update the regenerate route's headline building

In src/routes/issues.py, replace lines 106-113 in the regenerate function:

    articles_for_issue = (
        Article.query.filter(Article.id.in_(article_ids))
        .order_by(Article.pub_date.asc())
        .all()
    )
    headlines = [
        a.title for a in articles_for_issue
        if "Obituaries" not in json.loads(a.categories)
    ]
    categories_list = []
    for a in articles_for_issue:
        categories_list.extend(json.loads(a.categories))
  • Step 2: Run existing tests

Run: python -m pytest -v Expected: ALL PASS

  • Step 3: Commit
git add src/routes/issues.py
git commit -m "feat(cover): filter obituaries from headlines in issue regenerate"

Task 4: Filter obituaries from cover headlines in scheduler.py

Files:

  • Modify: src/scheduler.py:78-83

  • Step 1: Update auto-publish headline building

In src/scheduler.py, replace lines 78-83 in the _run_auto_publish method:

            article_ids = [a.id for a in articles]
            headlines = [
                a.title for a in articles
                if "Obituaries" not in json.loads(a.categories)
            ]

            categories_list = []
            for a in articles:
                categories_list.extend(json.loads(a.categories))
  • Step 2: Run existing tests

Run: python -m pytest -v Expected: ALL PASS

  • Step 3: Commit
git add src/scheduler.py
git commit -m "feat(cover): filter obituaries from headlines in auto-publish"

Task 5: Add test for obituary filtering

Files:

  • Test: tests/test_cover.py

  • Step 1: Write an integration test that verifies obituaries are excluded

Since the obituary filtering happens at the call site (not inside cover.py), this test validates the filtering pattern used across all three call sites. Add to tests/test_cover.py:

import json

def test_obituary_headlines_filtered():
    """Verify the filtering pattern used by publish/issues/scheduler call sites."""
    class FakeArticle:
        def __init__(self, title, categories):
            self.title = title
            self.categories = json.dumps(categories)

    articles = [
        FakeArticle("Town Meeting Approves Budget", ["Government"]),
        FakeArticle("John Smith, 85, beloved teacher", ["Obituaries"]),
        FakeArticle("Harbor Festival This Weekend", ["Culture"]),
        FakeArticle("Jane Doe, 92, retired nurse", ["Obituaries"]),
        FakeArticle("Panthers Win Championship", ["Sports"]),
    ]

    headlines = [
        a.title for a in articles
        if "Obituaries" not in json.loads(a.categories)
    ]

    assert len(headlines) == 3
    assert "John Smith, 85, beloved teacher" not in headlines
    assert "Jane Doe, 92, retired nurse" not in headlines
    assert "Town Meeting Approves Budget" in headlines
    assert "Harbor Festival This Weekend" in headlines
    assert "Panthers Win Championship" in headlines
  • Step 2: Run the new test

Run: python -m pytest tests/test_cover.py::test_obituary_headlines_filtered -v Expected: PASS

  • Step 3: Commit
git add tests/test_cover.py
git commit -m "test: add obituary headline filtering test"