Completed planning phases 0 and 1 for simple landing page feature. ## Plan Overview: **Approach**: Minimal addition to existing Flask app - reuse Product model, add one route, one template. No new dependencies or complexity. **Constitution Check**: ✅ All 5 principles satisfied - Specification-first development (spec.md complete) - Test-first discipline (TDD workflow defined) - Independent user stories (3 stories, all independently testable) - Simplicity (reuses existing Flask/Jinja2/Product architecture) - Documentation as code (all artifacts in specs/002-product-list/) ## Artifacts Created: ### Phase 0: Research (research.md) - Reuses infrastructure from feature 001 (Flask, Jinja2, file storage) - Single new decision: Product.load_active() method for filtering/sorting - Performance analysis: <100ms for 100 products (well under 1s target) ### Phase 1: Design & Contracts - **data-model.md**: Documents Product model extension (load_active method) - **contracts/landing-page.yaml**: OpenAPI contract for GET / route - **quickstart.md**: Developer implementation guide with: - Step-by-step implementation checklist - Code snippets for route, template, tests - TDD workflow (write tests → verify fail → implement → pass) - Manual verification checklist ### Agent Context - Updated CLAUDE.md with feature technologies (no new tech added) ## Implementation Summary: **New Files** (to be created): - app/routes/landing.py - Landing page route handler - app/templates/landing/index.html - Product list template - tests/contract/test_landing_routes.py - Contract tests (6 scenarios) - tests/integration/test_landing_flow.py - User journey test **Modified Files**: - app/models/product.py - Add load_active() class method - app/__init__.py - Register landing blueprint ## Key Technical Decisions: 1. **Filtering**: status=='active' AND submission_url_slug exists 2. **Sorting**: Alphabetical by name (case-insensitive), then product_id 3. **Empty State**: "No products are currently accepting feedback" message 4. **XSS Prevention**: Jinja2 auto-escaping (no manual escaping needed) 5. **Performance**: File I/O sufficient (<1s for 100 products, no caching) ## Next Steps: 1. Run /speckit.tasks to generate tasks.md 2. Run /speckit.implement to execute TDD workflow 3. Verify all tests pass 4. Manual verification checklist 5. Create pull request 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
8.1 KiB
Quickstart: Product Selection Landing Page
Feature: 002-product-list | For: Developers implementing this feature
Overview
Add a landing page at / that lists all active products for feedback submission. This is a simple addition to the existing Flask app: one route, one template, and corresponding tests.
Implementation Checklist
1. Extend Product Model
File: app/models/product.py
Add this class method:
@classmethod
def load_active(cls):
"""Load all active products, sorted alphabetically by name then product_id.
Returns:
list[Product]: Active products with valid submission_url_slug, sorted.
"""
all_products = cls.load_all()
active = [p for p in all_products
if p.status == 'active' and p.submission_url_slug]
return sorted(active, key=lambda p: (p.name.lower(), p.product_id))
Why: Centralizes filtering and sorting logic. Reusable if other features need active product lists.
2. Create Landing Route
File: app/routes/landing.py (new file)
Implementation:
from flask import Blueprint, render_template, current_app
from app.models.product import Product
landing_bp = Blueprint('landing', __name__)
@landing_bp.route('/')
def index():
"""Landing page showing all active products."""
try:
products = Product.load_active()
current_app.logger.info(
f'Landing page accessed: {len(products)} active products'
)
return render_template('landing/index.html', products=products)
except Exception as e:
current_app.logger.error(f'Error loading landing page: {e}', exc_info=True)
return render_template('landing/index.html', products=[])
Register blueprint in app/__init__.py:
from app.routes.landing import landing_bp
app.register_blueprint(landing_bp)
3. Create Landing Template
File: app/templates/landing/index.html (new file)
Template structure:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Select a Product - Reklamator</title>
<style>
body { font-family: sans-serif; max-width: 800px; margin: 40px auto; padding: 0 20px; }
h1 { color: #333; }
ul { list-style: none; padding: 0; }
li { margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 4px; }
a { font-size: 1.2em; color: #0066cc; text-decoration: none; }
a:hover { text-decoration: underline; }
p { margin: 5px 0 0 0; color: #666; }
.empty-state { color: #666; padding: 20px; text-align: center; }
</style>
</head>
<body>
<h1>Select a Product for Feedback</h1>
{% if products %}
<ul>
{% for product in products %}
<li>
<a href="/submit/{{ product.submission_url_slug }}">
{{ product.name }}
</a>
{% if product.description %}
<p>{{ product.description }}</p>
{% endif %}
</li>
{% endfor %}
</ul>
{% else %}
<p class="empty-state">
No products are currently accepting feedback. Please check back later.
</p>
{% endif %}
</body>
</html>
Key points:
- Jinja2 auto-escaping prevents XSS (product.name, product.description)
- No JavaScript (pure server-side rendering)
- Minimal inline CSS (no frameworks)
- Conditional rendering for empty state
- Only shows description if present (no placeholder text)
4. Write Contract Tests
File: tests/contract/test_landing_routes.py (new file)
Test scenarios to implement:
import pytest
import os
import yaml
from app.models.product import Product
@pytest.mark.contract
def test_get_landing_page_with_products(client, temp_data_dir):
"""T301: GET / returns 200 with product list"""
# Setup: Create 2 active products
# Assert: 200 OK, both products in HTML
pass
@pytest.mark.contract
def test_get_landing_page_no_products(client, temp_data_dir):
"""T302: GET / with no active products shows empty state"""
# Assert: 200 OK, contains "No products are currently accepting feedback"
pass
@pytest.mark.contract
def test_get_landing_page_filters_archived(client, temp_data_dir):
"""T303: GET / excludes archived products"""
# Setup: 1 active, 1 archived
# Assert: Only active product shown
pass
@pytest.mark.contract
def test_get_landing_page_sorting(client, temp_data_dir):
"""T304: GET / sorts products alphabetically (name, then product_id)"""
# Setup: Products with names "Zebra", "Apple", "apple" (different product_ids)
# Assert: Correct alphabetical order
pass
@pytest.mark.contract
def test_get_landing_page_xss_prevention(client, temp_data_dir):
"""T305: GET / escapes HTML in product names"""
# Setup: Product with name "<script>alert('xss')</script>"
# Assert: HTML is escaped, script not executed
pass
@pytest.mark.contract
def test_get_landing_page_missing_slug(client, temp_data_dir):
"""T306: GET / excludes products with missing submission_url_slug"""
# Setup: Product with submission_url_slug = None
# Assert: Product not shown in list
pass
5. Write Integration Tests
File: tests/integration/test_landing_flow.py (new file)
User journey test:
@pytest.mark.integration
def test_landing_to_submission_flow(client, temp_data_dir):
"""T307: Complete flow - landing page → product selection → submission form"""
# Step 1: Visit landing page, see products
# Step 2: Click product link
# Step 3: Verify redirected to /submit/{slug}
pass
Key Implementation Notes
XSS Prevention
- ✅ Jinja2 auto-escaping handles product names and descriptions
- ✅ No manual HTML escaping needed
- ✅ Test with
<script>tags in product names to verify
Empty State Handling
- ✅ Check
{% if products %}in template - ✅ Display message: "No products are currently accepting feedback. Please check back later."
- ✅ No blank page or error
Logging
- ✅ Log landing page access with product count
- ✅ Log errors if product loading fails
- ✅ Use
current_app.logger.info()for access logs
Backwards Compatibility
- ✅ Existing
/submit/{slug}routes unchanged - ✅ Direct product URLs still work
- ✅ Landing page is additive only
Performance
- ✅ Target: <1 second for up to 100 products
- ✅ File I/O ~10-50ms for 100 YAML files
- ✅ No caching needed for MVP
Testing Workflow (TDD)
Follow this order (Constitution Principle II):
- Write contract tests (test_landing_routes.py) - all should FAIL
- Verify tests fail - proves they test something meaningful
- Implement Product.load_active() method
- Implement landing route (landing.py)
- Create landing template (index.html)
- Run tests - contract tests should PASS
- Write integration tests (test_landing_flow.py) - should FAIL
- Fix any issues - integration tests should PASS
- Refactor while keeping tests green
Manual Verification Checklist
After all tests pass, manually verify:
- Visit
/- see product list or empty state - Click product link - redirected to
/submit/{slug} - Check with 0 active products - see empty message
- Check with 1 active product - see single product
- Check with 10+ active products - alphabetical order
- Check product with no description - no placeholder text
- Check product with long name - proper wrapping
- Check as anonymous user - page accessible
- Check as authenticated user - same page shown
- Check page source - no JavaScript present
Completion Criteria
✅ All contract tests passing ✅ All integration tests passing ✅ Product.load_active() method implemented ✅ Landing route registered and functional ✅ Landing template created with proper escaping ✅ Manual verification completed ✅ Code follows existing Flask/Jinja2 patterns ✅ No new dependencies added ✅ Documentation updated (this file)
Next: Commit to branch 002-product-list and create pull request