diff --git a/CLI_INTEGRATION_EXPLANATION.md b/CLI_INTEGRATION_EXPLANATION.md
deleted file mode 100644
index 1802129..0000000
--- a/CLI_INTEGRATION_EXPLANATION.md
+++ /dev/null
@@ -1,257 +0,0 @@
-# CLI Integration Explanation - Story 3.3
-
-## The Problem
-
-Story 3.3's `inject_interlinks()` function (and Stories 3.1-3.2) are **implemented and tested perfectly**, but they're **never called** in the actual batch generation workflow.
-
-## Current Workflow
-
-When you run:
-```bash
-uv run python main.py generate-batch --job-file jobs/example.json
-```
-
-Here's what actually happens:
-
-### Step-by-Step Current Flow
-
-```
-1. CLI Command (src/cli/commands.py)
- └─> generate_batch() function called
- └─> Creates BatchProcessor
- └─> BatchProcessor.process_job()
-
-2. BatchProcessor.process_job() (src/generation/batch_processor.py)
- └─> Reads job file
- └─> For each job:
- └─> _process_single_job()
- └─> Validates deployment targets
- └─> For each tier (tier1, tier2, tier3):
- └─> _process_tier()
-
-3. _process_tier()
- └─> For each article (1 to count):
- └─> _generate_single_article()
- ├─> Generate title
- ├─> Generate outline
- ├─> Generate content
- ├─> Augment if needed
- └─> SAVE to database
-
-4. END! ⚠️
-
- Nothing happens after articles are generated!
- No URLs, no tiered links, no interlinking!
-```
-
-## What's Missing
-
-After all articles are generated for a tier, we need to add Story 3.1-3.3:
-
-```python
-# THIS CODE DOES NOT EXIST YET!
-# Needs to be added at the end of _process_tier() or _process_single_job()
-
-# 1. Get all generated content for this batch
-content_records = self.content_repo.get_by_project_and_tier(project_id, tier_name)
-
-# 2. Assign sites (Story 3.1)
-from src.generation.site_assignment import assign_sites_to_batch
-assign_sites_to_batch(content_records, job, site_repo, bunny_client, project.main_keyword)
-
-# 3. Generate URLs (Story 3.1)
-from src.generation.url_generator import generate_urls_for_batch
-article_urls = generate_urls_for_batch(content_records, site_repo)
-
-# 4. Find tiered links (Story 3.2)
-from src.interlinking.tiered_links import find_tiered_links
-tiered_links = find_tiered_links(
- content_records, job_config, project_repo, content_repo, site_repo
-)
-
-# 5. Inject interlinks (Story 3.3)
-from src.interlinking.content_injection import inject_interlinks
-from src.database.repositories import ArticleLinkRepository
-link_repo = ArticleLinkRepository(session)
-inject_interlinks(
- content_records, article_urls, tiered_links,
- project, job_config, content_repo, link_repo
-)
-
-# 6. Apply templates (existing functionality)
-for content in content_records:
- content_generator.apply_template(content.id)
-```
-
-## Why This Matters
-
-### Current State
-✓ Articles are generated
-✗ Articles have NO internal links
-✗ Articles have NO tiered links
-✗ Articles have NO "See Also" section
-✗ Articles have NO final URLs assigned
-✗ Templates are NOT applied
-
-**Result**: Articles sit in database with raw HTML, no links, unusable for deployment
-
-### With Integration
-✓ Articles are generated
-✓ Sites are assigned to articles
-✓ Final URLs are generated
-✓ Tiered links are found
-✓ All links are injected
-✓ Templates are applied
-✓ Articles are ready for deployment
-
-**Result**: Complete, interlinked articles ready for Story 4.x deployment
-
-## Where to Add Integration
-
-### Option 1: End of `_process_tier()` (RECOMMENDED)
-Add the integration code at line 162 (after the article generation loop):
-
-```python
-def _process_tier(self, project_id, tier_name, tier_config, ...):
- # ... existing article generation loop ...
-
- # NEW: Post-generation interlinking
- click.echo(f" {tier_name}: Injecting interlinks for {tier_config.count} articles...")
- self._inject_tier_interlinks(project_id, tier_name, job, debug)
-```
-
-Then create new method:
-```python
-def _inject_tier_interlinks(self, project_id, tier_name, job, debug):
- """Inject interlinks for all articles in a tier"""
- # Get all articles for this tier
- content_records = self.content_repo.get_by_project_and_tier(
- project_id, tier_name
- )
-
- if not content_records:
- click.echo(f" Warning: No articles found for {tier_name}")
- return
-
- # Steps 1-6 from above...
-```
-
-### Option 2: End of `_process_single_job()`
-Add integration after ALL tiers are generated (processes entire job at once):
-
-```python
-def _process_single_job(self, job, job_idx, debug, continue_on_error):
- # ... existing tier processing ...
-
- # NEW: Process all tiers together
- click.echo(f"\nPost-processing: Injecting interlinks...")
- for tier_name in job.tiers.keys():
- self._inject_tier_interlinks(job.project_id, tier_name, job, debug)
-```
-
-## Why It Wasn't Integrated Yet
-
-Looking at the story implementations, it appears:
-
-1. **Story 3.1** (URL Generation) - Functions exist but not integrated
-2. **Story 3.2** (Tiered Links) - Functions exist but not integrated
-3. **Story 3.3** (Content Injection) - Functions exist but not integrated
-
-This suggests the stories focused on **building the functionality** with the expectation that **Story 4.x (Deployment)** would integrate everything together.
-
-## Impact of Missing Integration
-
-### Tests Still Pass ✓
-- Unit tests test functions in isolation
-- Integration tests use the functions directly
-- All 42 tests pass because the **functions work perfectly**
-
-### But Real Usage Fails ✗
-When you actually run `generate-batch`:
-- Articles are generated
-- They're saved to database
-- But they have no links, no URLs, nothing
-- Story 4.x deployment would fail because articles aren't ready
-
-## Effort to Fix
-
-**Time Estimate**: 30-60 minutes
-
-**Tasks**:
-1. Add imports to `batch_processor.py` (2 minutes)
-2. Create `_inject_tier_interlinks()` method (15 minutes)
-3. Add call at end of `_process_tier()` (2 minutes)
-4. Test with real job file (10 minutes)
-5. Debug any issues (10-20 minutes)
-
-**Complexity**: Low - just wiring existing functions together
-
-## Testing the Integration
-
-After adding integration:
-
-```bash
-# 1. Run batch generation
-uv run python main.py generate-batch \
- --job-file jobs/test_small.json \
- --username admin \
- --password yourpass
-
-# 2. Check database for links
-uv run python -c "
-from src.database.session import db_manager
-from src.database.repositories import ArticleLinkRepository
-
-session = db_manager.get_session()
-link_repo = ArticleLinkRepository(session)
-links = link_repo.get_all()
-print(f'Total links: {len(links)}')
-for link in links[:5]:
- print(f' {link.link_type}: {link.anchor_text} -> {link.to_url or link.to_content_id}')
-session.close()
-"
-
-# 3. Verify articles have links in content
-uv run python -c "
-from src.database.session import db_manager
-from src.database.repositories import GeneratedContentRepository
-
-session = db_manager.get_session()
-content_repo = GeneratedContentRepository(session)
-articles = content_repo.get_all(limit=1)
-if articles:
- print('Sample article content:')
- print(articles[0].content[:500])
- print(f'Contains links: {\"` menu with Home link
-- Template line 113: `
` tag
-- Each link uses article title as anchor text
-- Creates internal links (`to_content_id`)
-
-**Test Evidence**:
-```
-test_inject_see_also_with_multiple_articles PASSED
-test_inject_see_also_with_single_article PASSED
-test_large_batch PASSED (20 articles, 19 See Also links each)
-```
-
-### 4. Anchor Text Configuration
-**Status**: PASSED ✓
-
-**Behavior**:
-- **Default mode**: Uses tier-based anchor text
- - T1: Main keyword variations
- - T2: Related searches
- - T3: Main keyword variations
- - T4+: Entities
-- **Override mode**: Replaces tier-based with custom text
-- **Append mode**: Adds custom text to tier-based defaults
-
-**Test Evidence**:
-```
-test_default_mode PASSED
-test_override_mode PASSED (unit + integration)
-test_append_mode PASSED (unit + integration)
-```
-
-### 5. Database Integration
-**Status**: PASSED ✓
-
-**Behavior**:
-- Updates `generated_content.content` with final HTML
-- Creates `ArticleLink` records for all links
-- Correctly categorizes link types:
- - `tiered`: Money site or lower-tier links
- - `homepage`: Homepage links
- - `wheel_see_also`: See Also section links
-- Handles internal (to_content_id) vs external (to_url) links
-
-**Test Evidence**:
-```
-test_all_link_types_recorded PASSED
-test_internal_vs_external_links PASSED
-test_tier1_batch_with_money_site_links PASSED
-```
-
----
-
-## Template Integration
-
-**Status**: PASSED ✓
-
-All 4 HTML templates updated with navigation menu:
-- `src/templating/templates/basic.html` ✓
-- `src/templating/templates/modern.html` ✓
-- `src/templating/templates/classic.html` ✓
-- `src/templating/templates/minimal.html` ✓
-
-**Navigation Structure**:
-```html
-
-```
-
-Each template has custom styling matching its theme.
-
----
-
-## Edge Cases & Error Handling
-
-### Tested Edge Cases
-- [x] Empty content records (graceful skip)
-- [x] Single article batch (no See Also section)
-- [x] Large batch (20+ articles)
-- [x] Missing URL for content (skip with warning)
-- [x] Missing money site URL (skip with error)
-- [x] No valid paragraphs for fallback insertion
-- [x] Anchor text not found in content (fallback insertion)
-- [x] Existing links in content (skip, don't double-link)
-- [x] Malformed HTML (BeautifulSoup handles gracefully)
-
-### Error Handling Verification
-```python
-# Test evidence:
-test_empty_content_records PASSED
-test_missing_url_for_content PASSED
-test_tier1_no_money_site PASSED
-test_no_valid_paragraphs PASSED
-test_no_anchors PASSED
-```
-
----
-
-## Performance Metrics
-
-### Test Execution Times
-- **Unit Tests**: ~1.66s (33 tests)
-- **Integration Tests**: ~2.40s (9 tests)
-- **Total**: ~4.3s for complete test suite
-
-### Database Operations
-- Efficient batch processing
-- Single transaction per article update
-- Bulk link creation
-- No N+1 query issues observed
-
----
-
-## Known Issues & Limitations
-
-### None Critical
-All known limitations are by design:
-
-1. **First Occurrence Only**: Only links first occurrence of anchor text
- - **Why**: Prevents over-optimization and keyword stuffing
- - **Status**: Working as intended
-
-2. **Random Lower-Tier Selection**: T2+ articles randomly select 2-4 lower-tier links
- - **Why**: Natural link distribution
- - **Status**: Working as intended
-
-3. **Fallback Insertion**: If anchor text not found, inserts at random position
- - **Why**: Ensures link injection even if anchor text not naturally in content
- - **Status**: Working as intended
-
----
-
-## Regression Testing
-
-### Dependencies Verified
-- [x] Story 3.1 (URL Generation): Integration tests pass
-- [x] Story 3.2 (Tiered Links): Integration tests pass
-- [x] Story 2.x (Content Generation): No regressions
-- [x] Database Models: No schema issues
-- [x] Templates: All 4 templates render correctly
-
-### No Breaking Changes
-- All existing tests still pass (42/42)
-- No API changes to public functions
-- Backward compatible with existing job configs
-
----
-
-## Production Readiness Checklist
-
-- [x] **All Tests Pass**: 42/42 (100%)
-- [x] **Zero Linter Errors**: Clean code
-- [x] **Comprehensive Test Coverage**: Unit + integration
-- [x] **Error Handling**: Graceful degradation
-- [x] **Documentation**: Complete implementation summary
-- [x] **Database Integration**: All CRUD operations tested
-- [x] **Edge Cases**: Thoroughly tested
-- [x] **Performance**: Sub-5s test execution
-- [x] **Type Safety**: Full type hints
-- [x] **Logging**: Comprehensive logging at all levels
-- [x] **Template Updates**: All 4 templates updated
-
----
-
-## Integration Status
-
-### Current State
-Story 3.3 functions are **implemented and tested** but **NOT YET INTEGRATED** into the main CLI workflow.
-
-**Evidence**:
-- `generate-batch` command in `src/cli/commands.py` uses `BatchProcessor`
-- `BatchProcessor` generates content but does NOT call:
- - `generate_urls_for_batch()` (Story 3.1)
- - `find_tiered_links()` (Story 3.2)
- - `inject_interlinks()` (Story 3.3)
-
-**Impact**:
-- Functions work perfectly in isolation (as proven by tests)
-- Need integration into batch generation workflow
-- Likely will be integrated in Story 4.x (deployment)
-
-### Integration Points Needed
-```python
-# After batch generation completes, need to add:
-# 1. Assign sites to articles (Story 3.1)
-assign_sites_to_batch(content_records, job, site_repo, bunny_client, project.main_keyword)
-
-# 2. Generate URLs (Story 3.1)
-article_urls = generate_urls_for_batch(content_records, site_repo)
-
-# 3. Find tiered links (Story 3.2)
-tiered_links = find_tiered_links(content_records, job_config, project_repo, content_repo, site_repo)
-
-# 4. Inject interlinks (Story 3.3)
-inject_interlinks(content_records, article_urls, tiered_links, project, job_config, content_repo, link_repo)
-
-# 5. Apply templates (existing)
-for content in content_records:
- content_generator.apply_template(content.id)
-```
-
----
-
-## Recommendations
-
-### Ready for Production
-Story 3.3 is **APPROVED** for production deployment with one caveat:
-
-**Caveat**: Requires CLI integration in batch generation workflow (likely Story 4.x scope)
-
-### Next Steps
-1. **CRITICAL**: Integrate Story 3.1-3.3 into `generate-batch` CLI command
- - Add calls after content generation completes
- - Add error handling for integration failures
- - Add CLI output for URL/link generation progress
-2. **Story 4.x**: Deployment (can now use final HTML with all links)
-3. **Future Analytics**: Can leverage `article_links` table for link analysis
-4. **Future Pages**: Create About, Privacy, Contact pages to match nav menu
-
-### Optional Enhancements (Low Priority)
-1. **Link Density Control**: Add configurable max links per article
-2. **Custom See Also Heading**: Make "See Also" heading configurable
-3. **Link Position Strategy**: Add preference for link placement (intro/body/conclusion)
-4. **Anchor Text Variety**: Add more sophisticated anchor text rotation
-
----
-
-## Sign-Off
-
-**QA Status**: PASSED ✓
-**Approved By**: AI Code Review Assistant
-**Date**: October 21, 2025
-
-**Summary**: Story 3.3 implementation exceeds quality standards with 100% test pass rate, zero defects, comprehensive edge case handling, and production-ready code quality.
-
-**Recommendation**: APPROVE FOR DEPLOYMENT
-
----
-
-## Appendix: Test Output
-
-### Full Test Suite Execution
-```
-===== test session starts =====
-platform win32 -- Python 3.13.3, pytest-8.4.2
-collected 42 items
-
-tests/unit/test_content_injection.py::TestExtractHomepageUrl PASSED [5/5]
-tests/unit/test_content_injection.py::TestInsertBeforeClosingTags PASSED [3/3]
-tests/unit/test_content_injection.py::TestFindAndWrapAnchorText PASSED [5/5]
-tests/unit/test_content_injection.py::TestInsertLinkIntoRandomParagraph PASSED [3/3]
-tests/unit/test_content_injection.py::TestGetAnchorTextsForTier PASSED [4/4]
-tests/unit/test_content_injection.py::TestTryInjectLink PASSED [3/3]
-tests/unit/test_content_injection.py::TestInjectSeeAlsoSection PASSED [2/2]
-tests/unit/test_content_injection.py::TestInjectHomepageLink PASSED [2/2]
-tests/unit/test_content_injection.py::TestInjectTieredLinks PASSED [3/3]
-tests/unit/test_content_injection.py::TestInjectInterlinks PASSED [3/3]
-
-tests/integration/test_content_injection_integration.py::TestTier1ContentInjection PASSED [2/2]
-tests/integration/test_content_injection_integration.py::TestTier2ContentInjection PASSED [1/1]
-tests/integration/test_content_injection_integration.py::TestAnchorTextConfigOverrides PASSED [2/2]
-tests/integration/test_content_injection_integration.py::TestDifferentBatchSizes PASSED [2/2]
-tests/integration/test_content_injection_integration.py::TestLinkDatabaseRecords PASSED [2/2]
-
-===== 42 passed in 2.64s =====
-```
-
-### Linter Output
-```
-No linter errors found.
-```
-
----
-
-*End of QA Report*
-
diff --git a/QA_REPORT_STORY_3.4.md b/QA_REPORT_STORY_3.4.md
deleted file mode 100644
index b0c51b8..0000000
--- a/QA_REPORT_STORY_3.4.md
+++ /dev/null
@@ -1,283 +0,0 @@
-# QA Report: Story 3.4 - Generate Boilerplate Site Pages
-
-## QA Summary
-**Date:** October 22, 2025
-**Story:** Story 3.4 - Generate Boilerplate Site Pages
-**Status:** PASSED - Ready for Production
-**QA Engineer:** AI Assistant
-
-## Executive Summary
-Story 3.4 implementation has been thoroughly tested and meets all acceptance criteria. All 37 tests pass successfully, database migration is complete, and the implementation follows the design specifications. The feature generates boilerplate pages (about, contact, privacy) for sites with proper template integration and database persistence.
-
-## Test Results
-
-### Unit Tests
-**Status:** PASSED
-**Tests Run:** 26
-**Passed:** 26
-**Failed:** 0
-**Coverage:** >80% on new modules
-
-#### Test Breakdown
-1. **test_page_templates.py** (6 tests) - PASSED
- - Content generation for all page types (about, contact, privacy)
- - Unknown page type handling
- - HTML structure validation
- - Domain parameter handling
-
-2. **test_site_page_generator.py** (9 tests) - PASSED
- - Domain extraction from custom and b-cdn hostnames
- - Page generation with different templates
- - Skipping existing pages
- - Default template fallback
- - Content structure validation
- - Page titles
- - Error handling
-
-3. **test_site_page_repository.py** (11 tests) - PASSED
- - CRUD operations
- - Duplicate page prevention
- - Update and delete operations
- - Exists checks
- - Not found scenarios
-
-### Integration Tests
-**Status:** PASSED
-**Tests Run:** 11
-**Passed:** 11
-**Failed:** 0
-
-#### Test Coverage
-- Full flow: site creation → page generation → database storage
-- Template application (basic, modern, classic, minimal)
-- Duplicate prevention via unique constraint
-- Multiple sites with separate pages
-- Custom domain handling
-- Page retrieval by type
-- Page existence checks
-
-### Database Migration
-**Status:** PASSED
-
-#### Migration Verification
-- `site_pages` table exists ✓
-- All required columns present:
- - `id` ✓
- - `site_deployment_id` ✓
- - `page_type` ✓
- - `content` ✓
- - `created_at` ✓
- - `updated_at` ✓
-- Indexes created:
- - `idx_site_pages_site` ✓
- - `idx_site_pages_type` ✓
-- Foreign key constraint: CASCADE delete ✓
-- Unique constraint: `(site_deployment_id, page_type)` ✓
-
-### Linter Checks
-**Status:** PASSED
-No linter errors found in:
-- `src/generation/site_page_generator.py`
-- `src/generation/page_templates.py`
-- `scripts/backfill_site_pages.py`
-- `src/generation/site_provisioning.py`
-
-## Acceptance Criteria Verification
-
-### Core Functionality
-- [x] Function generates three boilerplate pages for a given site
-- [x] Pages created AFTER articles but BEFORE deployment
-- [x] Each page uses same template as articles for that site
-- [x] Pages stored in database for deployment
-- [x] Pages associated with correct site via `site_deployment_id`
-
-### Page Content Requirements
-- [x] About page: Empty with heading only `
About Us
`
-- [x] Contact page: Empty with heading only `
Contact
`
-- [x] Privacy page: Empty with heading only `
Privacy Policy
`
-- [x] All pages use template structure with navigation
-
-### Template Integration
-- [x] Uses same template engine as article content
-- [x] Reads template from `site.template_name` field
-- [x] Pages use same template as articles on same site
-- [x] Includes navigation menu
-
-### Database Storage
-- [x] `site_pages` table with proper schema
-- [x] Foreign key to `site_deployments` with CASCADE delete
-- [x] Unique constraint on `(site_deployment_id, page_type)`
-- [x] Indexes on `site_deployment_id` and `page_type`
-- [x] Each site can have one of each page type
-
-### URL Generation
-- [x] Pages use simple filenames: `about.html`, `contact.html`, `privacy.html`
-- [x] Full URLs: `https://{hostname}/about.html`
-- [x] No slug generation needed
-
-### Integration Point
-- [x] Integrated with site provisioning
-- [x] Generates pages when new sites are created
-- [x] Graceful error handling (doesn't break site creation)
-- [x] Backfill script for existing sites
-
-### Two Use Cases
-- [x] One-time backfill: Script available with dry-run mode
-- [x] Ongoing generation: Auto-generates for new sites during provisioning
-
-## Code Quality Assessment
-
-### Design Patterns
-- **Repository Pattern:** Properly implemented with ISitePageRepository interface
-- **Separation of Concerns:** Clean separation between page content, generation, and persistence
-- **Dependency Injection:** Optional parameters for backward compatibility
-- **Error Handling:** Graceful degradation with proper logging
-
-### Code Organization
-- **Modularity:** New functionality in separate modules
-- **Naming:** Clear, descriptive function and variable names
-- **Documentation:** Comprehensive docstrings on all functions
-- **Type Hints:** Proper type annotations throughout
-
-### Best Practices
-- **DRY Principle:** Reusable helper functions (`get_domain_from_site`)
-- **Single Responsibility:** Each module has clear purpose
-- **Testability:** All functions easily testable (demonstrated by 37 passing tests)
-- **Logging:** Appropriate INFO/WARNING/ERROR levels
-
-## Integration Verification
-
-### Site Provisioning Integration
-- [x] `create_bunnynet_site()` accepts optional parameters
-- [x] `provision_keyword_sites()` passes parameters through
-- [x] `create_generic_sites()` passes parameters through
-- [x] Backward compatible (optional parameters)
-- [x] Pages generated automatically after site creation
-
-### Backfill Script
-- [x] Admin authentication required
-- [x] Dry-run mode available
-- [x] Progress reporting
-- [x] Batch processing support
-- [x] Error handling for individual failures
-
-## Performance Considerations
-
-### Resource Usage
-- Page generation adds ~1-2 seconds per site (3 pages × template application)
-- Database operations optimized with indexes
-- Unique constraint prevents duplicate work
-- Minimal impact on batch processing (only for new sites)
-
-### Scalability
-- Can handle backfilling hundreds of sites
-- Batch processing with progress checkpoints
-- Individual site failures don't stop entire process
-
-## Known Issues
-**None identified**
-
-## Warnings/Notes
-
-### Deprecation Warnings
-- SQLAlchemy emits 96 deprecation warnings about `datetime.utcnow()`
-- **Impact:** Low - This is a SQLAlchemy internal issue, not related to Story 3.4
-- **Recommendation:** Update SQLAlchemy or adjust datetime usage in future sprint
-
-### Unrelated Test Failures
-- Some tests in other modules have import errors (ContentGenerationService, ContentRuleEngine)
-- **Impact:** None on Story 3.4 functionality
-- **Recommendation:** Address in separate ticket
-
-## Recommendations
-
-### Immediate Actions
-1. **Update Story Status:** Change from "Awaiting QA" to "Complete"
-2. **Commit Changes:** All modified files are working correctly
-3. **Documentation:** Implementation summary is accurate and complete
-
-### Future Enhancements (Optional)
-1. **Homepage Generation:** Add `index.html` generation (deferred to Epic 4)
-2. **Custom Page Content:** Allow projects to override generic templates
-3. **Multi-language Support:** Generate pages in different languages
-4. **CLI Edit Command:** Add command to update page content for specific sites
-5. **Fix Deprecation Warnings:** Update to `datetime.now(datetime.UTC)`
-
-### Production Readiness Checklist
-- [x] All tests passing
-- [x] Database migration successful
-- [x] No linter errors
-- [x] Backward compatible
-- [x] Error handling implemented
-- [x] Logging implemented
-- [x] Documentation complete
-- [x] Integration verified
-- [x] Performance acceptable
-
-## Test Execution Details
-
-### Command
-```bash
-uv run pytest tests/unit/test_site_page_generator.py \
- tests/unit/test_site_page_repository.py \
- tests/unit/test_page_templates.py \
- tests/integration/test_site_page_integration.py -v
-```
-
-### Results
-```
-37 passed, 96 warnings in 2.23s
-```
-
-### Database Verification
-```bash
-uv run python scripts/migrate_add_site_pages.py
-```
-
-**Output:**
-```
-[SUCCESS] Migration completed successfully!
-```
-
-## Files Reviewed
-
-### New Files
-- `src/generation/site_page_generator.py` - Core generation logic ✓
-- `src/generation/page_templates.py` - Minimal content templates ✓
-- `scripts/migrate_add_site_pages.py` - Database migration ✓
-- `scripts/backfill_site_pages.py` - Backfill script ✓
-- `tests/unit/test_site_page_generator.py` - Unit tests ✓
-- `tests/unit/test_site_page_repository.py` - Repository tests ✓
-- `tests/unit/test_page_templates.py` - Template tests ✓
-- `tests/integration/test_site_page_integration.py` - Integration tests ✓
-
-### Modified Files
-- `src/database/models.py` - Added SitePage model ✓
-- `src/database/interfaces.py` - Added ISitePageRepository interface ✓
-- `src/database/repositories.py` - Added SitePageRepository implementation ✓
-- `src/generation/site_provisioning.py` - Integrated page generation ✓
-- `src/generation/site_assignment.py` - Pass through parameters ✓
-- `docs/stories/story-3.4-boilerplate-site-pages.md` - Story documentation ✓
-- `STORY_3.4_IMPLEMENTATION_SUMMARY.md` - Implementation summary ✓
-
-## Conclusion
-
-**Story 3.4 is APPROVED for production.**
-
-All acceptance criteria have been met, tests are passing, and the implementation is robust, well-documented, and follows best practices. The feature successfully generates boilerplate pages for sites, fixing broken navigation links from Story 3.3.
-
-The code is:
-- **Functional:** All features work as designed
-- **Tested:** 37/37 tests passing
-- **Maintainable:** Clean code with good documentation
-- **Scalable:** Can handle hundreds of sites
-- **Backward Compatible:** Optional parameters don't break existing code
-
-**Total Effort:** 14 story points (as estimated)
-**Test Coverage:** 37 tests (26 unit + 11 integration)
-**Status:** Ready for Epic 4 (Deployment)
-
----
-
-**QA Sign-off:** Story 3.4 is complete and production-ready.
-
diff --git a/README.md b/README.md
index f92efd1..e2d3079 100644
--- a/README.md
+++ b/README.md
@@ -406,6 +406,69 @@ uv run python scripts/add_robots_txt_to_buckets.py --provider bunny
The script is idempotent (safe to run multiple times) and will overwrite existing robots.txt files. It continues processing remaining buckets if one fails and reports all failures at the end.
+### Update Index Pages and Sitemaps
+
+Automatically generate or update `index.html` and `sitemap.xml` files for all storage buckets (both S3 and Bunny). The script:
+
+- Lists all HTML files in each bucket's root directory
+- Extracts titles from `` tags (or formats filenames as fallback)
+- Generates article listings sorted by most recent modification date
+- Creates or updates `index.html` with article links in `
`
+- Generates `sitemap.xml` with industry-standard settings (priority, changefreq, lastmod)
+- Tracks last run timestamps to avoid unnecessary updates
+- Excludes boilerplate pages: `index.html`, `about.html`, `privacy.html`, `contact.html`
+
+**Usage:**
+
+```bash
+# Preview what would be updated (recommended first)
+uv run python scripts/update_index_pages.py --dry-run
+
+# Update all buckets
+uv run python scripts/update_index_pages.py
+
+# Only process S3 buckets
+uv run python scripts/update_index_pages.py --provider s3
+
+# Only process Bunny storage zones
+uv run python scripts/update_index_pages.py --provider bunny
+
+# Force update even if no changes detected
+uv run python scripts/update_index_pages.py --force
+
+# Test on specific site
+uv run python scripts/update_index_pages.py --hostname example.com
+
+# Limit number of sites (useful for testing)
+uv run python scripts/update_index_pages.py --limit 10
+```
+
+**How it works:**
+
+1. Queries database for all site deployments
+2. Lists HTML files in root directory (excludes subdirectories and boilerplate pages)
+3. Checks if content has changed since last run (unless `--force` is used)
+4. Downloads and parses HTML files to extract titles
+5. Generates article listing HTML (sorted by most recent first)
+6. Creates new `index.html` or updates existing one (inserts into `
`)
+7. Generates `sitemap.xml` with all HTML files and proper metadata
+8. Uploads both files to the bucket
+9. Saves state to `.update_index_state.json` for tracking
+
+**Sitemap standards:**
+- Priority: `1.0` for homepage (`index.html`), `0.8` for other pages
+- Change frequency: `weekly` for all pages
+- Last modified dates from file metadata
+- Includes all HTML files in root directory
+
+**Customizing article listing HTML:**
+
+The article listing format can be easily customized by editing the `generate_article_listing_html()` function in `scripts/update_index_pages.py`. The function includes detailed documentation and examples for common variations (cards, dates, descriptions, etc.).
+
+**State tracking:**
+
+The script maintains state in `scripts/.update_index_state.json` to track when each site was last updated. This prevents unnecessary regeneration when content hasn't changed. Use `--force` to bypass this check.
+
### Check Last Generated Content
```bash
uv run python check_last_gen.py
@@ -644,10 +707,12 @@ Verify `storage_zone_password` in database (set during site provisioning)
## Documentation
- **CLI Command Reference**: `docs/CLI_COMMAND_REFERENCE.md` - Comprehensive documentation for all CLI commands
-- Product Requirements: `docs/prd.md`
-- Architecture: `docs/architecture/`
-- Implementation Summaries: `STORY_*.md` files
-- Quick Start Guides: `*_QUICKSTART.md` files
+- **Job Configuration Schema**: `docs/job-schema.md` - Complete reference for job configuration files
+- **Product Requirements**: `docs/prd.md` - Product requirements and epics
+- **Architecture**: `docs/architecture/` - System architecture documentation
+- **Story Specifications**: `docs/stories/` - Current story specifications
+- **Technical Debt**: `docs/technical-debt.md` - Known technical debt items
+- **Historical Documentation**: `docs/archive/` - Archived implementation summaries, QA reports, and analysis documents
### Regenerating CLI Documentation
diff --git a/STORY_2.5_IMPLEMENTATION_SUMMARY.md b/STORY_2.5_IMPLEMENTATION_SUMMARY.md
deleted file mode 100644
index 101e615..0000000
--- a/STORY_2.5_IMPLEMENTATION_SUMMARY.md
+++ /dev/null
@@ -1,142 +0,0 @@
-# Story 2.5: Deployment Target Assignment - Implementation Summary
-
-## Status
-**COMPLETED** - All acceptance criteria met, 100% test coverage
-
-## Overview
-Implemented deployment target assignment functionality that allows job configurations to specify which generated tier1 articles should be assigned to specific sites. **Only tier1 articles can be assigned to deployment targets** - tier2/tier3 always get `site_deployment_id = null`. The implementation uses a simple round-robin assignment strategy where the first N tier1 articles are assigned to N deployment targets, and remaining tier1 articles get null assignment.
-
-## Changes Made
-
-### 1. Job Configuration Schema (`src/generation/job_config.py`)
-- Added `deployment_targets` field (optional array of strings) to `Job` dataclass
-- Added validation to ensure `deployment_targets` is an array of strings
-- Job configuration now supports specifying custom hostnames for deployment target assignment
-
-### 2. Deployment Assignment Logic (`src/generation/deployment_assignment.py`) - NEW FILE
-Created new module with three core functions:
-
-- `resolve_hostname_to_id()` - Resolves a hostname to its site_deployment_id
-- `validate_and_resolve_targets()` - Validates all hostnames at job start (fail-fast approach)
-- `assign_site_for_article()` - Implements round-robin assignment logic
-
-### 3. Database Repository Updates (`src/database/repositories.py`)
-- Updated `GeneratedContentRepository.create()` to accept optional `site_deployment_id` parameter
-- Maintains backward compatibility - parameter defaults to `None`
-
-### 4. Batch Processor Integration (`src/generation/batch_processor.py`)
-- Added `site_deployment_repo` parameter to `BatchProcessor.__init__()`
-- Validates deployment targets at job start before generating any content
-- **Only applies deployment targets to tier1 articles** - tier2/tier3 always get null
-- Assigns `site_deployment_id` to each tier1 article based on its index
-- Logs assignment decisions at INFO level
-- Passes `site_deployment_id` to repository when creating content
-
-### 5. CLI Updates (`src/cli/commands.py`)
-- Updated `generate-batch` command to initialize and pass `SiteDeploymentRepository` to `BatchProcessor`
-- Fixed merge conflict markers in the file
-
-### 6. Example Job Configuration (`jobs/example_deployment_targets.json`) - NEW FILE
-Created example job file demonstrating the `deployment_targets` field with 3 sites and 10 articles.
-
-## Test Coverage
-
-### Unit Tests (`tests/unit/test_deployment_assignment.py`) - NEW FILE
-13 unit tests covering:
-- Hostname resolution (valid and invalid)
-- Target validation (empty lists, valid hostnames, invalid hostnames, type checking)
-- Round-robin assignment logic (edge cases, overflow, single target)
-- The 10-article, 3-target scenario from the story
-
-### Integration Tests (`tests/integration/test_deployment_target_assignment.py`) - NEW FILE
-10 integration tests covering:
-- Job config parsing with deployment_targets
-- Job config validation (type checking, missing field handling)
-- Batch processor validation at job start
-- End-to-end assignment logic
-- Repository backward compatibility
-- **Tier1-only deployment target assignment** (tier2+ always get null)
-
-**Total Test Results: 23/23 tests passing**
-
-## Assignment Logic Example
-
-Job with tier1 (10 articles), tier2 (100 articles), and 3 deployment targets:
-
-**Tier1 articles:**
-```
-Article 0 → www.domain1.com (site_deployment_id = 5)
-Article 1 → www.domain2.com (site_deployment_id = 8)
-Article 2 → www.domain3.com (site_deployment_id = 12)
-Articles 3-9 → null (no assignment)
-```
-
-**Tier2 articles:**
-```
-All 100 articles → null (tier2+ never get deployment targets)
-```
-
-## Usage Example
-
-```json
-{
- "jobs": [{
- "project_id": 2,
- "deployment_targets": [
- "www.domain1.com",
- "www.domain2.com",
- "www.domain3.com"
- ],
- "tiers": {
- "tier1": {
- "count": 10
- }
- }
- }]
-}
-```
-
-## Error Handling
-
-The implementation provides clear error messages:
-
-1. **Invalid hostnames**: "Deployment targets not found in database: invalid.com. Please ensure these sites exist using 'list-sites' command."
-2. **Missing repository**: "deployment_targets specified but SiteDeploymentRepository not provided"
-3. **Invalid configuration**: Validates array type and string elements with descriptive errors
-
-## Backward Compatibility
-
-- All changes are backward compatible
-- Jobs without `deployment_targets` continue to work as before (all articles get `site_deployment_id = null`)
-- Existing tests remain passing
-- No database schema changes required (field already existed from Story 2.4)
-
-## Integration with Story 2.4
-
-The implementation correctly integrates with Story 2.4's template selection logic:
-- If `site_deployment_id` is set → Story 2.4 uses mapped/random template for that site
-- If `site_deployment_id` is null → Story 2.4 uses random template selection
-
-## Acceptance Criteria Verification
-
-✅ Job configuration supports optional `deployment_targets` array of custom_hostnames
-✅ Round-robin assignment: articles 0 through N-1 get assigned, N+ get null
-✅ Missing `deployment_targets` → all articles get null
-✅ `site_deployment_id` stored in GeneratedContent at creation time
-✅ Invalid hostnames cause graceful errors with clear messages
-✅ Non-existent hostnames cause graceful errors
-✅ Validation occurs at job start (fail-fast)
-✅ Assignment decisions logged at INFO level
-
-## Files Created
-- `src/generation/deployment_assignment.py`
-- `tests/unit/test_deployment_assignment.py`
-- `tests/integration/test_deployment_target_assignment.py`
-- `jobs/example_deployment_targets.json`
-
-## Files Modified
-- `src/generation/job_config.py`
-- `src/generation/batch_processor.py`
-- `src/database/repositories.py`
-- `src/cli/commands.py`
-
diff --git a/STORY_3.1_IMPLEMENTATION_SUMMARY.md b/STORY_3.1_IMPLEMENTATION_SUMMARY.md
deleted file mode 100644
index 38bafbf..0000000
--- a/STORY_3.1_IMPLEMENTATION_SUMMARY.md
+++ /dev/null
@@ -1,266 +0,0 @@
-# Story 3.1 Implementation Summary
-
-## Overview
-Implemented URL generation and site assignment for batch content generation, including full auto-creation capabilities and priority-based site assignment.
-
-## What Was Implemented
-
-### 1. Database Schema Changes
-- **Modified**: `src/database/models.py`
- - Made `custom_hostname` nullable in `SiteDeployment` model
- - Added unique constraint to `pull_zone_bcdn_hostname`
- - Updated `__repr__` to handle both custom and bcdn hostnames
-
-- **Migration Script**: `scripts/migrate_story_3.1.sql`
- - SQL script to update existing databases
- - Run this on your dev database before testing
-
-### 2. Repository Layer Updates
-- **Modified**: `src/database/interfaces.py`
- - Changed `custom_hostname` to optional parameter in `create()` signature
- - Added `get_by_bcdn_hostname()` method signature
- - Updated `exists()` to check both hostname types
-
-- **Modified**: `src/database/repositories.py`
- - Made `custom_hostname` parameter optional with default `None`
- - Implemented `get_by_bcdn_hostname()` method
- - Updated `exists()` to query both custom and bcdn hostnames
-
-### 3. Template Service Update
-- **Modified**: `src/templating/service.py`
- - Line 92: Changed to `hostname = site_deployment.custom_hostname or site_deployment.pull_zone_bcdn_hostname`
- - Now handles sites with only bcdn hostnames
-
-### 4. CLI Updates
-- **Modified**: `src/cli/commands.py`
- - Updated `sync-sites` command to import sites without custom domains
- - Removed filter that skipped bcdn-only sites
- - Now imports all bunny.net sites (with or without custom domains)
-
-### 5. Site Provisioning Module (NEW)
-- **Created**: `src/generation/site_provisioning.py`
- - `generate_random_suffix()`: Creates random 4-char suffixes
- - `slugify_keyword()`: Converts keywords to URL-safe slugs
- - `create_bunnynet_site()`: Creates Storage Zone + Pull Zone via API
- - `provision_keyword_sites()`: Pre-creates sites for specific keywords
- - `create_generic_sites()`: Creates generic sites on-demand
-
-### 6. URL Generator Module (NEW)
-- **Created**: `src/generation/url_generator.py`
- - `generate_slug()`: Converts article titles to URL-safe slugs
- - `generate_urls_for_batch()`: Generates complete URLs for all articles in batch
- - Handles custom domains and bcdn hostnames
- - Returns full URL mappings with metadata
-
-### 7. Job Config Extensions
-- **Modified**: `src/generation/job_config.py`
- - Added `tier1_preferred_sites: Optional[List[str]]` field
- - Added `auto_create_sites: bool` field (default: False)
- - Added `create_sites_for_keywords: Optional[List[Dict]]` field
- - Full validation for all new fields
-
-### 8. Site Assignment Module (NEW)
-- **Created**: `src/generation/site_assignment.py`
- - `assign_sites_to_batch()`: Main assignment function with full priority system
- - `_get_keyword_sites()`: Helper to match sites by keyword
- - **Priority system**:
- - Tier1: preferred sites → keyword sites → random
- - Tier2+: keyword sites → random
- - Auto-creates sites when pool is insufficient (if enabled)
- - Prevents duplicate assignments within same batch
-
-### 9. Comprehensive Tests
-- **Created**: `tests/unit/test_url_generator.py` - URL generation tests
-- **Created**: `tests/unit/test_site_provisioning.py` - Site creation tests
-- **Created**: `tests/unit/test_site_assignment.py` - Assignment logic tests
-- **Created**: `tests/unit/test_job_config_extensions.py` - Config parsing tests
-- **Created**: `tests/integration/test_story_3_1_integration.py` - Full workflow tests
-
-### 10. Example Job Config
-- **Created**: `jobs/example_story_3.1_full_features.json`
- - Demonstrates all new features
- - Ready-to-use template
-
-## How to Use
-
-### Step 1: Migrate Your Database
-Run the migration script on your development database:
-
-```sql
--- From scripts/migrate_story_3.1.sql
-ALTER TABLE site_deployments MODIFY COLUMN custom_hostname VARCHAR(255) NULL;
-ALTER TABLE site_deployments ADD CONSTRAINT uq_pull_zone_bcdn_hostname UNIQUE (pull_zone_bcdn_hostname);
-```
-
-### Step 2: Sync Existing Bunny.net Sites
-Import your 400+ existing bunny.net buckets:
-
-```bash
-uv run python main.py sync-sites --admin-user your_admin --dry-run
-```
-
-Review the output, then run without `--dry-run` to import.
-
-### Step 3: Create a Job Config
-Use the new fields in your job configuration:
-
-```json
-{
- "jobs": [{
- "project_id": 1,
- "tiers": {
- "tier1": {"count": 10}
- },
- "tier1_preferred_sites": ["www.premium.com"],
- "auto_create_sites": true,
- "create_sites_for_keywords": [
- {"keyword": "engine repair", "count": 3}
- ]
- }]
-}
-```
-
-### Step 4: Use in Your Workflow
-In your content generation workflow:
-
-```python
-from src.generation.site_assignment import assign_sites_to_batch
-from src.generation.url_generator import generate_urls_for_batch
-
-# After content generation, assign sites
-assign_sites_to_batch(
- content_records=generated_articles,
- job=job_config,
- site_repo=site_repository,
- bunny_client=bunny_client,
- project_keyword=project.main_keyword
-)
-
-# Generate URLs
-urls = generate_urls_for_batch(
- content_records=generated_articles,
- site_repo=site_repository
-)
-
-# urls is a list of:
-# [{
-# "content_id": 1,
-# "title": "How to Fix Your Engine",
-# "url": "https://www.example.com/how-to-fix-your-engine.html",
-# "tier": "tier1",
-# "slug": "how-to-fix-your-engine",
-# "hostname": "www.example.com"
-# }, ...]
-```
-
-## Site Assignment Priority Logic
-
-### For Tier1 Articles:
-1. **Preferred Sites** (from `tier1_preferred_sites`) - if specified
-2. **Keyword Sites** (matching article keyword in site name)
-3. **Random** from available pool
-
-### For Tier2+ Articles:
-1. **Keyword Sites** (matching article keyword in site name)
-2. **Random** from available pool
-
-### Auto-Creation:
-If `auto_create_sites: true` and pool is insufficient:
-- Creates minimum number of generic sites needed
-- Uses project main keyword in site names
-- Creates via bunny.net API (Storage Zone + Pull Zone)
-
-## URL Structure
-
-### With Custom Domain:
-```
-https://www.example.com/how-to-fix-your-engine.html
-```
-
-### With Bunny.net CDN Only:
-```
-https://mysite123.b-cdn.net/how-to-fix-your-engine.html
-```
-
-## Slug Generation Rules
-- Lowercase
-- Replace spaces with hyphens
-- Remove special characters
-- Max 100 characters
-- Fallback: `article-{content_id}` if empty
-
-## Testing
-
-Run the tests:
-
-```bash
-# Unit tests
-uv run pytest tests/unit/test_url_generator.py
-uv run pytest tests/unit/test_site_provisioning.py
-uv run pytest tests/unit/test_site_assignment.py
-uv run pytest tests/unit/test_job_config_extensions.py
-
-# Integration tests
-uv run pytest tests/integration/test_story_3_1_integration.py
-
-# All Story 3.1 tests
-uv run pytest tests/ -k "story_3_1 or url_generator or site_provisioning or site_assignment or job_config_extensions"
-```
-
-## Key Features
-
-### Simple Over Complex
-- No fuzzy keyword matching (as requested)
-- Straightforward priority system
-- Clear error messages
-- Minimal dependencies
-
-### Full Auto-Creation
-- Pre-create sites for specific keywords
-- Auto-create generic sites when needed
-- All sites use bunny.net API
-
-### Full Priority System
-- Tier1 preferred sites
-- Keyword-based matching
-- Random assignment fallback
-
-### Flexible Hostnames
-- Supports custom domains
-- Supports bcdn-only sites
-- Automatically chooses correct hostname
-
-## Production Deployment
-
-When moving to production:
-1. The model changes will automatically apply (SQLAlchemy will create tables correctly)
-2. No additional migration scripts needed
-3. Just ensure your production `.env` has `BUNNY_ACCOUNT_API_KEY` set
-4. Run `sync-sites` to import existing bunny.net infrastructure
-
-## Files Changed/Created
-
-### Modified (8 files):
-- `src/database/models.py`
-- `src/database/interfaces.py`
-- `src/database/repositories.py`
-- `src/templating/service.py`
-- `src/cli/commands.py`
-- `src/generation/job_config.py`
-
-### Created (9 files):
-- `scripts/migrate_story_3.1.sql`
-- `src/generation/site_provisioning.py`
-- `src/generation/url_generator.py`
-- `src/generation/site_assignment.py`
-- `tests/unit/test_url_generator.py`
-- `tests/unit/test_site_provisioning.py`
-- `tests/unit/test_site_assignment.py`
-- `tests/unit/test_job_config_extensions.py`
-- `tests/integration/test_story_3_1_integration.py`
-- `jobs/example_story_3.1_full_features.json`
-- `STORY_3.1_IMPLEMENTATION_SUMMARY.md`
-
-## Total Effort
-Completed all 10 tasks from the story specification.
-
diff --git a/STORY_3.1_QUICKSTART.md b/STORY_3.1_QUICKSTART.md
deleted file mode 100644
index e105f1e..0000000
--- a/STORY_3.1_QUICKSTART.md
+++ /dev/null
@@ -1,173 +0,0 @@
-# Story 3.1 Quick Start Guide
-
-## Implementation Complete!
-
-All features for Story 3.1 have been implemented and tested. 44 tests passing.
-
-## What You Need to Do
-
-### 1. Run Database Migration (Dev Environment)
-
-```sql
--- Connect to your MySQL database and run:
-ALTER TABLE site_deployments MODIFY COLUMN custom_hostname VARCHAR(255) NULL;
-ALTER TABLE site_deployments ADD CONSTRAINT uq_pull_zone_bcdn_hostname UNIQUE (pull_zone_bcdn_hostname);
-```
-
-Or run: `mysql -u your_user -p your_database < scripts/migrate_story_3.1.sql`
-
-### 2. Import Existing Bunny.net Sites
-
-Now you can import your 400+ existing bunny.net buckets (with or without custom domains):
-
-```bash
-# Dry run first to see what will be imported
-uv run python main.py sync-sites --admin-user your_admin --dry-run
-
-# Actually import
-uv run python main.py sync-sites --admin-user your_admin
-```
-
-This will now import ALL bunny.net sites, including those without custom domains.
-
-### 3. Run Tests
-
-```bash
-# Run all Story 3.1 tests
-uv run pytest tests/unit/test_url_generator.py \
- tests/unit/test_site_provisioning.py \
- tests/unit/test_site_assignment.py \
- tests/unit/test_job_config_extensions.py \
- tests/integration/test_story_3_1_integration.py \
- -v
-```
-
-Expected: 44 tests passing
-
-### 4. Use New Features
-
-#### Example Job Config
-
-Create a job config file using the new features:
-
-```json
-{
- "jobs": [{
- "project_id": 1,
- "tiers": {
- "tier1": {"count": 10},
- "tier2": {"count": 50}
- },
- "deployment_targets": ["www.primary.com"],
- "tier1_preferred_sites": [
- "www.premium-site.com",
- "site123.b-cdn.net"
- ],
- "auto_create_sites": true,
- "create_sites_for_keywords": [
- {"keyword": "engine repair", "count": 3}
- ]
- }]
-}
-```
-
-#### In Your Code
-
-```python
-from src.generation.site_assignment import assign_sites_to_batch
-from src.generation.url_generator import generate_urls_for_batch
-
-# After content generation
-assign_sites_to_batch(
- content_records=batch_articles,
- job=job,
- site_repo=site_repo,
- bunny_client=bunny_client,
- project_keyword=project.main_keyword,
- region="DE"
-)
-
-# Generate URLs
-url_mappings = generate_urls_for_batch(
- content_records=batch_articles,
- site_repo=site_repo
-)
-
-# Use the URLs
-for url_info in url_mappings:
- print(f"{url_info['title']}: {url_info['url']}")
-```
-
-## New Features Available
-
-### 1. Sites Without Custom Domains
-- Import and use bunny.net sites that only have `.b-cdn.net` hostnames
-- No custom domain required
-- Perfect for your 400+ existing buckets
-
-### 2. Auto-Creation of Sites
-- Set `auto_create_sites: true` in job config
-- System creates sites automatically when pool is insufficient
-- Uses project keyword in site names
-
-### 3. Keyword-Based Site Creation
-- Pre-create sites for specific keywords
-- Example: `{"keyword": "engine repair", "count": 3}`
-- Creates 3 sites with "engine-repair" in the name
-
-### 4. Tier1 Preferred Sites
-- Specify premium sites for tier1 articles
-- Example: `"tier1_preferred_sites": ["www.premium.com"]`
-- Tier1 articles assigned to these first
-
-### 5. Smart Site Assignment
-**Tier1 Priority:**
-1. Preferred sites (if specified)
-2. Keyword-matching sites
-3. Random from pool
-
-**Tier2+ Priority:**
-1. Keyword-matching sites
-2. Random from pool
-
-### 6. URL Generation
-- Automatic slug generation from titles
-- Works with custom domains OR bcdn hostnames
-- Format: `https://domain.com/article-slug.html`
-
-## File Changes Summary
-
-### Modified (6 core files):
-- `src/database/models.py` - Nullable custom_hostname
-- `src/database/interfaces.py` - Optional custom_hostname in interface
-- `src/database/repositories.py` - New get_by_bcdn_hostname() method
-- `src/templating/service.py` - Handles both hostname types
-- `src/cli/commands.py` - sync-sites imports all sites
-- `src/generation/job_config.py` - New config fields
-
-### Created (3 new modules):
-- `src/generation/site_provisioning.py` - Creates bunny.net sites
-- `src/generation/url_generator.py` - Generates URLs and slugs
-- `src/generation/site_assignment.py` - Assigns sites to articles
-
-### Created (5 test files):
-- `tests/unit/test_url_generator.py` (14 tests)
-- `tests/unit/test_site_provisioning.py` (8 tests)
-- `tests/unit/test_site_assignment.py` (9 tests)
-- `tests/unit/test_job_config_extensions.py` (8 tests)
-- `tests/integration/test_story_3_1_integration.py` (5 tests)
-
-## Production Deployment
-
-When you deploy to production:
-1. Model changes automatically apply (SQLAlchemy creates tables correctly)
-2. No special migration needed - just deploy the code
-3. Run `sync-sites` to import your bunny.net infrastructure
-4. Start using the new features
-
-## Support
-
-See `STORY_3.1_IMPLEMENTATION_SUMMARY.md` for detailed documentation.
-
-Example job config: `jobs/example_story_3.1_full_features.json`
-
diff --git a/STORY_3.2_IMPLEMENTATION_SUMMARY.md b/STORY_3.2_IMPLEMENTATION_SUMMARY.md
deleted file mode 100644
index a955eae..0000000
--- a/STORY_3.2_IMPLEMENTATION_SUMMARY.md
+++ /dev/null
@@ -1,187 +0,0 @@
-# Story 3.2: Find Tiered Links - Implementation Summary
-
-## Status
-Completed
-
-## What Was Implemented
-
-### 1. Database Models
-- **Added `money_site_url` to Project model** - Stores the client's actual website URL for tier 1 articles to link to
-- **Created `ArticleLink` model** - Tracks all link relationships between articles (tiered, wheel, homepage)
-
-### 2. Database Repositories
-- **Extended `ProjectRepository`** - Now accepts `money_site_url` in the data dict during creation
-- **Extended `GeneratedContentRepository`** - Added filter for site_deployment_id in `get_by_project_and_tier()`
-- **Created `ArticleLinkRepository`** - Full CRUD operations for article link tracking
- - `create()` - Create internal or external links
- - `get_by_source_article()` - Get all outbound links from an article
- - `get_by_target_article()` - Get all inbound links to an article
- - `get_by_link_type()` - Get all links of a specific type
- - `delete()` - Remove a link
-
-### 3. Job Configuration
-- **Extended `Job` dataclass** - Added optional `tiered_link_count_range` field
-- **Validation** - Validates that min >= 1 and max >= min
-- **Defaults** - If not specified, uses `{min: 2, max: 4}`
-
-### 4. Core Functionality
-Created `src/interlinking/tiered_links.py` with:
-- **`find_tiered_links()`** - Main function to find tiered links for a batch
- - For tier 1: Returns the money site URL
- - For tier 2+: Returns random selection of lower-tier article URLs
- - Respects project boundaries (only queries same project)
- - Applies link count configuration
- - Handles edge cases (insufficient articles, missing money site URL)
-
-### 5. Tests
-- **22 unit tests** in `tests/unit/test_tiered_links.py` - All passing
-- **8 unit tests** in `tests/unit/test_article_link_repository.py` - All passing
-- **9 integration tests** in `tests/integration/test_story_3_2_integration.py` - All passing
-- **Total: 39 tests, all passing**
-
-## Usage Examples
-
-### Finding Tiered Links for Tier 1 Batch
-```python
-from src.interlinking.tiered_links import find_tiered_links
-
-# Tier 1 articles link to the money site
-result = find_tiered_links(tier1_content_records, job, project_repo, content_repo, site_repo)
-# Returns: {
-# "tier": 1,
-# "money_site_url": "https://www.mymoneysite.com"
-# }
-```
-
-### Finding Tiered Links for Tier 2 Batch
-```python
-# Tier 2 articles link to random tier 1 articles
-result = find_tiered_links(tier2_content_records, job, project_repo, content_repo, site_repo)
-# Returns: {
-# "tier": 2,
-# "lower_tier": 1,
-# "lower_tier_urls": [
-# "https://site1.b-cdn.net/article-1.html",
-# "https://site2.b-cdn.net/article-2.html",
-# "https://site3.b-cdn.net/article-3.html"
-# ]
-# }
-```
-
-### Job Config with Custom Link Count
-```json
-{
- "jobs": [{
- "project_id": 1,
- "tiers": {
- "tier1": {"count": 5},
- "tier2": {"count": 10}
- },
- "tiered_link_count_range": {
- "min": 3,
- "max": 5
- }
- }]
-}
-```
-
-### Recording Links in Database
-```python
-from src.database.repositories import ArticleLinkRepository
-
-link_repo = ArticleLinkRepository(session)
-
-# Record tier 1 article linking to money site
-link_repo.create(
- from_content_id=tier1_article.id,
- to_content_id=None,
- to_url="https://www.moneysite.com",
- link_type="tiered"
-)
-
-# Record tier 2 article linking to tier 1 article
-link_repo.create(
- from_content_id=tier2_article.id,
- to_content_id=tier1_article.id,
- to_url=None,
- link_type="tiered"
-)
-
-# Query all links from an article
-outbound_links = link_repo.get_by_source_article(article.id)
-```
-
-## Database Schema Changes
-
-### Project Table
-```sql
-ALTER TABLE projects ADD COLUMN money_site_url VARCHAR(500) NULL;
-CREATE INDEX idx_projects_money_site_url ON projects(money_site_url);
-```
-
-### Article Links Table (New)
-```sql
-CREATE TABLE article_links (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- from_content_id INTEGER NOT NULL,
- to_content_id INTEGER NULL,
- to_url TEXT NULL,
- anchor_text TEXT NULL, -- Added in Story 4.5
- link_type VARCHAR(20) NOT NULL,
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- FOREIGN KEY (from_content_id) REFERENCES generated_content(id) ON DELETE CASCADE,
- FOREIGN KEY (to_content_id) REFERENCES generated_content(id) ON DELETE CASCADE,
- CHECK (to_content_id IS NOT NULL OR to_url IS NOT NULL)
-);
-
-CREATE INDEX idx_article_links_from ON article_links(from_content_id);
-CREATE INDEX idx_article_links_to ON article_links(to_content_id);
-CREATE INDEX idx_article_links_type ON article_links(link_type);
-```
-
-## Link Types
-- `tiered` - Link from tier N to tier N-1 (or money site for tier 1)
-- `wheel_next` - Link to next article in wheel structure
-- `wheel_prev` - Link to previous article in wheel structure
-- `homepage` - Link to site homepage
-
-## Key Features
-1. **Project Isolation** - Only queries articles from the same project
-2. **Random Selection** - Randomly selects articles within configured range
-3. **Flexible Configuration** - Supports both range (min-max) and exact counts
-4. **Error Handling** - Clear error messages for missing data
-5. **Warning Logs** - Logs warnings when fewer articles available than requested
-6. **URL Generation** - Integrates with Story 3.1 URL generation
-
-## Next Steps (Future Stories)
-- Story 3.3 will use `find_tiered_links()` for actual content injection
-- Story 3.3 will populate `article_links` table with wheel and homepage links
-- Story 4.2 will log tiered links after deployment
-- Future: Analytics dashboard using `article_links` data
-
-## Files Created/Modified
-
-### Created
-- `src/interlinking/tiered_links.py`
-- `tests/unit/test_tiered_links.py`
-- `tests/unit/test_article_link_repository.py`
-- `tests/integration/test_story_3_2_integration.py`
-- `jobs/example_story_3.2_tiered_links.json`
-- `STORY_3.2_IMPLEMENTATION_SUMMARY.md` (this file)
-
-### Modified
-- `src/database/models.py` - Added `money_site_url` to Project, added `ArticleLink` model
-- `src/database/interfaces.py` - Added `IArticleLinkRepository` interface
-- `src/database/repositories.py` - Extended `ProjectRepository`, added `ArticleLinkRepository`
-- `src/generation/job_config.py` - Added `tiered_link_count_range` to Job config
-
-## Test Coverage
-All acceptance criteria from the story are covered by tests:
-- Tier 1 returns money site URL
-- Tier 2+ queries lower tier from same project
-- Custom link count ranges work
-- Error handling for missing data
-- Warning logs for insufficient articles
-- ArticleLink CRUD operations
-- Integration with URL generation
-
diff --git a/STORY_3.3_COMPLETE.md b/STORY_3.3_COMPLETE.md
deleted file mode 100644
index ce0a5e5..0000000
--- a/STORY_3.3_COMPLETE.md
+++ /dev/null
@@ -1,327 +0,0 @@
-# Story 3.3: Content Interlinking Injection - COMPLETE ✅
-
-**Status**: Implemented, Integrated, Tested, and Production-Ready
-**Date Completed**: October 21, 2025
-
----
-
-## Summary
-
-Story 3.3 is **100% COMPLETE** including:
-- ✅ Core implementation (`src/interlinking/content_injection.py`)
-- ✅ Full test coverage (42 tests, 100% passing)
-- ✅ CLI integration (`src/generation/batch_processor.py`)
-- ✅ Real-world validation (tested with live batch generation)
-- ✅ Zero linter errors
-- ✅ Documentation updated
-
----
-
-## What Was Delivered
-
-### 1. Core Functionality
-**File**: `src/interlinking/content_injection.py` (410 lines)
-
-Three types of link injection:
-- **Tiered Links**: T1→money site, T2+→lower-tier articles
-- **Homepage Links**: All articles→`/index.html` with "Home" anchor
-- **See Also Section**: Each article→all other batch articles
-
-Features:
-- Tier-based anchor text with job config overrides (default/override/append)
-- Case-insensitive anchor text matching
-- First occurrence only (prevents over-optimization)
-- Fallback insertion when anchor not found
-- Database link tracking (`article_links` table)
-
-### 2. Template Updates
-All 4 HTML templates now have navigation menus:
-- `src/templating/templates/basic.html`
-- `src/templating/templates/modern.html`
-- `src/templating/templates/classic.html`
-- `src/templating/templates/minimal.html`
-
-Each template has theme-appropriate styling for:
-```html
-
-```
-
-### 3. Test Coverage
-**Unit Tests**: `tests/unit/test_content_injection.py` (33 tests)
-- Homepage URL extraction
-- HTML insertion
-- Anchor text finding & wrapping
-- Link injection fallback
-- Anchor text config modes
-- All helper functions
-
-**Integration Tests**: `tests/integration/test_content_injection_integration.py` (9 tests)
-- Full T1 batch with money site links
-- T2 batch linking to T1 articles
-- Anchor text config overrides
-- Different batch sizes (1-20 articles)
-- Database link records
-- Internal vs external links
-
-**Result**: 42/42 tests passing (100%)
-
-### 4. CLI Integration
-**File**: `src/generation/batch_processor.py`
-
-Added complete post-processing pipeline:
-1. **Site Assignment** (Story 3.1) - Automatic assignment from pool
-2. **URL Generation** (Story 3.1) - Final public URLs
-3. **Tiered Links** (Story 3.2) - Find money site or lower-tier URLs
-4. **Content Injection** (Story 3.3) - Inject all links
-5. **Template Application** - Apply HTML templates
-
-### 5. Database Integration
-Updated `src/database/repositories.py`:
-- Added `require_site` parameter to `get_by_project_and_tier()`
-- Backward compatible (default maintains existing behavior)
-
-All links tracked in `article_links` table:
-- `link_type="tiered"` - Money site or lower-tier links
-- `link_type="homepage"` - Homepage links to `/index.html`
-- `link_type="wheel_see_also"` - See Also section links
-
----
-
-## How It Works Now
-
-### Before Story 3.3
-```
-uv run python main.py generate-batch --job-file jobs/example.json
-
-Result:
- - Articles generated ✓
- - Raw HTML, no links ✗
- - Not ready for deployment ✗
-```
-
-### After Story 3.3
-```
-uv run python main.py generate-batch --job-file jobs/example.json
-
-Result:
- - Articles generated ✓
- - Sites auto-assigned ✓
- - URLs generated ✓
- - Tiered links injected ✓
- - Homepage links injected ✓
- - See Also sections added ✓
- - Templates applied ✓
- - Ready for deployment! ✓
-```
-
----
-
-## Acceptance Criteria - All Met ✅
-
-From the story requirements:
-
-### Core Functionality
-- [x] Function takes raw HTML, URL list, tiered links, and project data
-- [x] **Wheel Links**: "See Also" section with ALL other batch articles
-- [x] **Homepage Links**: Links to site's homepage (`/index.html`)
-- [x] **Tiered Links**: T1→money site, T2+→lower-tier articles
-
-### Input Requirements
-- [x] Accepts raw HTML content from Epic 2
-- [x] Accepts article URL list from Story 3.1
-- [x] Accepts tiered links object from Story 3.2
-- [x] Accepts project data for anchor text
-- [x] Handles batch tier information
-
-### Output Requirements
-- [x] Final HTML with all links injected
-- [x] Updated content stored in database
-- [x] Link relationships recorded in `article_links` table
-
-### Technical Requirements
-- [x] Case-insensitive anchor text matching
-- [x] Links first occurrence only
-- [x] Fallback insertion when anchor not found
-- [x] Job config overrides (default/override/append)
-- [x] Preserves HTML structure
-- [x] Safe HTML parsing (BeautifulSoup)
-
----
-
-## Files Changed
-
-### Created
-- `src/interlinking/content_injection.py` (410 lines)
-- `tests/unit/test_content_injection.py` (363 lines, 33 tests)
-- `tests/integration/test_content_injection_integration.py` (469 lines, 9 tests)
-- `STORY_3.3_IMPLEMENTATION_SUMMARY.md` (240 lines)
-- `docs/stories/story-3.3-content-interlinking-injection.md` (342 lines)
-- `QA_REPORT_STORY_3.3.md` (482 lines)
-- `STORY_3.3_QA_SUMMARY.md` (247 lines)
-- `INTEGRATION_COMPLETE.md` (245 lines)
-- `CLI_INTEGRATION_EXPLANATION.md` (258 lines)
-- `INTEGRATION_GAP_VISUAL.md` (242 lines)
-
-### Modified
-- `src/templating/templates/basic.html` - Added navigation menu
-- `src/templating/templates/modern.html` - Added navigation menu
-- `src/templating/templates/classic.html` - Added navigation menu
-- `src/templating/templates/minimal.html` - Added navigation menu
-- `src/generation/batch_processor.py` - Added post-processing pipeline (~100 lines)
-- `src/database/repositories.py` - Added `require_site` parameter
-
-**Total**: 10 new files, 6 modified files, ~3,000 lines of code/tests/docs
-
----
-
-## Quality Metrics
-
-- **Test Coverage**: 42/42 tests passing (100%)
-- **Linter Errors**: 0
-- **Code Quality**: Excellent
-- **Documentation**: Comprehensive
-- **Integration**: Complete
-- **Production Ready**: Yes
-
----
-
-## Validation Results
-
-### Automated Tests
-```
-42 passed in 2.54s
-✅ All unit tests pass
-✅ All integration tests pass
-✅ Zero linter errors
-```
-
-### Real-World Test
-```
-Job: 2 articles, 1 deployment target
-
-Results:
- Article 1:
- - Site: www.testsite.com (via deployment_targets)
- - Links: 9 (tiered + homepage + See Also)
- - Template: classic
- - Status: Ready ✅
-
- Article 2:
- - Site: www.testsite2.com (auto-assigned from pool)
- - Links: 6 (tiered + homepage + See Also)
- - Template: minimal
- - Status: Ready ✅
-
-Database:
- - 15 link records created
- - All link types present (tiered, homepage, wheel_see_also)
- - Internal and external links tracked correctly
-```
-
----
-
-## Usage Example
-
-```bash
-# 1. Create a job file
-cat > jobs/my_batch.json << 'EOF'
-{
- "jobs": [{
- "project_id": 1,
- "deployment_targets": ["www.mysite.com"],
- "tiers": {
- "tier1": {
- "count": 5,
- "min_word_count": 2000,
- "max_word_count": 2500
- }
- }
- }]
-}
-EOF
-
-# 2. Run batch generation
-uv run python main.py generate-batch \
- --job-file jobs/my_batch.json \
- --username admin \
- --password yourpass
-
-# Output shows:
-# ✓ Articles generated
-# ✓ Sites assigned
-# ✓ URLs generated
-# ✓ Tiered links found
-# ✓ Interlinks injected ← Story 3.3!
-# ✓ Templates applied
-
-# 3. Articles are now deployment-ready with:
-# - Full URLs
-# - Money site links
-# - Homepage links
-# - See Also sections
-# - HTML templates applied
-```
-
----
-
-## Dependencies
-
-### Runtime
-- BeautifulSoup4 (HTML parsing)
-- Story 3.1 (URL generation, site assignment)
-- Story 3.2 (Tiered link finding)
-- Story 2.x (Content generation)
-- Existing anchor text generator
-
-### Development
-- pytest (testing)
-- All dependencies satisfied and tested
-
----
-
-## Future Enhancements (Optional)
-
-Story 3.3 is complete as specified. Potential future improvements:
-
-1. **Link Density Control**: Configurable max links per article
-2. **Custom See Also Heading**: Make "See Also" heading configurable
-3. **Link Position Strategy**: Preference for intro/body/conclusion placement
-4. **Anchor Text Variety**: More sophisticated rotation strategies
-5. ~~**About/Privacy/Contact Pages**: Create pages to match nav menu links~~ ✅ **PROMOTED TO STORY 3.4**
-
-None of these are required for Story 3.3 completion.
-
-### Story 3.4 Emerged from Story 3.3
-During Story 3.3 implementation, we added navigation menus to all templates that link to `about.html`, `contact.html`, and `privacy.html`. However, these pages don't exist, creating broken links. This was identified as a high-priority issue and promoted to **Story 3.4: Boilerplate Site Pages**.
-
-See: `docs/stories/story-3.4-boilerplate-site-pages.md`
-
----
-
-## Sign-Off
-
-**Implementation**: COMPLETE ✅
-**Integration**: COMPLETE ✅
-**Testing**: COMPLETE ✅
-**Documentation**: COMPLETE ✅
-**QA**: PASSED ✅
-
-**Story 3.3 is DONE and ready for production.**
-
-Next: **Story 4.x** - Deployment (final HTML with all links is ready)
-
----
-
-**Completed by**: AI Code Assistant
-**Completed on**: October 21, 2025
-**Total effort**: ~5 hours (implementation + integration + testing + documentation)
-
-*This story delivers a complete, tested, production-ready content interlinking system that automatically creates fully interlinked article batches ready for deployment.*
-
diff --git a/STORY_3.3_IMPLEMENTATION_SUMMARY.md b/STORY_3.3_IMPLEMENTATION_SUMMARY.md
deleted file mode 100644
index 0421a0a..0000000
--- a/STORY_3.3_IMPLEMENTATION_SUMMARY.md
+++ /dev/null
@@ -1,241 +0,0 @@
-# Story 3.3: Content Interlinking Injection - Implementation Summary
-
-## Status
-✅ **COMPLETE & INTEGRATED** - All acceptance criteria met, all tests passing, CLI integration complete
-
-**Date Completed**: October 21, 2025
-
-## What Was Implemented
-
-### Core Module: `src/interlinking/content_injection.py`
-
-Main function: `inject_interlinks()` - Injects three types of links into article HTML:
-
-1. **Tiered Links** (Money Site / Lower Tier Articles)
- - Tier 1: Links to money site URL
- - Tier 2+: Links to 2-4 random lower-tier articles
- - Uses tier-appropriate anchor text from `anchor_text_generator.py`
- - Supports job config overrides (default/override/append modes)
- - Searches for anchor text in content (case-insensitive)
- - Wraps first occurrence or inserts via fallback
-
-2. **Homepage Links**
- - Links to `/index.html` on the article's domain
- - Uses "Home" as anchor text
- - Searches for "Home" in article content or inserts it
-
-3. **"See Also" Section**
- - Added after last `` tag
- - Links to ALL other articles in the batch
- - Each link uses article title as anchor text
- - Formatted as `