Five-task plan covering pixel-width truncation, font size bump, and obituary filtering across three call sites. Made-with: Cursor
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_overlaysfunction) -
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_widthand 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"