# Story 5.4: Public Posts Display ## Epic Reference **Epic 5:** Posts/Blog System ## User Story As a **website visitor**, I want **to view published blog posts**, So that **I can read legal updates and articles from the firm**. ## Story Context ### Existing System Integration - **Integrates with:** posts table (published only) - **Technology:** Livewire Volt (public routes) - **Follows pattern:** Public content display - **Touch points:** Navigation, homepage ### Assumptions - Post model exists with `published()` scope (from Story 5.1) - Post model has locale-aware `title`, `body`, and `excerpt` accessors (from Story 5.1) - Bilingual infrastructure is in place (from Epic 1) - Base navigation includes link to posts section (from Epic 1) ## Acceptance Criteria ### Posts Listing Page - [ ] Public access (no login required) - [ ] Display in reverse chronological order - [ ] Each post card shows: - Title - Publication date - Excerpt (first ~150 characters) - Read more link - [ ] Pagination for many posts ### Individual Post Page - [ ] Full post content displayed - [ ] Clean, readable typography - [ ] Publication date shown - [ ] Back to posts list link ### Language Support - [ ] Content displayed based on current site language - [ ] Title and body in selected language - [ ] Date formatting per locale ### Design Requirements - [ ] Responsive design (mobile-friendly) - [ ] Professional typography - [ ] Consistent with brand colors ### Quality Requirements - [ ] Only published posts visible - [ ] Fast loading - [ ] SEO-friendly URLs (optional per PRD) - [ ] Tests for display logic ## Technical Notes ### Routes ```php // routes/web.php (public) Route::get('/posts', \App\Livewire\Posts\Index::class)->name('posts.index'); Route::get('/posts/{post}', \App\Livewire\Posts\Show::class)->name('posts.show'); ``` ### Posts Index Component ```php Post::published() ->latest() ->paginate(10), ]; } }; ?>
{{ __('posts.title') }}
@forelse($posts as $post) @empty

{{ __('posts.no_posts') }}

@endforelse
{{ $posts->links() }}
``` ### Post Show Component ```php status === 'published', 404); $this->post = $post; } }; ?>
{{ $post->title }}
{!! $post->body !!}
``` ### Prose Styling ```css /* In app.css */ .prose-navy { --tw-prose-headings: theme('colors.navy'); --tw-prose-links: theme('colors.gold'); --tw-prose-bold: theme('colors.navy'); } .prose-navy a { text-decoration: underline; } .prose-navy a:hover { color: theme('colors.gold-light'); } ``` ## Testing Requirements ### Feature Tests ```php use App\Models\Post; use Livewire\Volt\Volt; test('posts listing page shows only published posts', function () { Post::factory()->create(['status' => 'published', 'title_en' => 'Published Post']); Post::factory()->create(['status' => 'draft', 'title_en' => 'Draft Post']); $this->get(route('posts.index')) ->assertOk() ->assertSee('Published Post') ->assertDontSee('Draft Post'); }); test('posts displayed in reverse chronological order', function () { $older = Post::factory()->create(['status' => 'published', 'created_at' => now()->subDays(5)]); $newer = Post::factory()->create(['status' => 'published', 'created_at' => now()]); $this->get(route('posts.index')) ->assertOk() ->assertSeeInOrder([$newer->title, $older->title]); }); test('posts listing page paginates results', function () { Post::factory()->count(15)->create(['status' => 'published']); $this->get(route('posts.index')) ->assertOk() ->assertSee('Next'); // pagination link }); test('individual post page shows full content', function () { $post = Post::factory()->create(['status' => 'published']); $this->get(route('posts.show', $post)) ->assertOk() ->assertSee($post->title) ->assertSee($post->body); }); test('individual post page returns 404 for unpublished posts', function () { $post = Post::factory()->create(['status' => 'draft']); $this->get(route('posts.show', $post)) ->assertNotFound(); }); test('individual post page returns 404 for non-existent posts', function () { $this->get(route('posts.show', 999)) ->assertNotFound(); }); ``` ### Language/RTL Tests - [ ] Arabic content displays correctly with RTL direction - [ ] English content displays correctly with LTR direction - [ ] Date formatting respects locale (`translatedFormat`) - [ ] Back link text displays in current language ### Responsive Tests - [ ] Mobile layout renders correctly (single column, touch-friendly) - [ ] Tablet layout renders correctly - [ ] Typography remains readable at all breakpoints ### Key Test Scenarios | Scenario | Expected Result | |----------|-----------------| | Visit /posts as guest | Shows published posts list | | Visit /posts with no posts | Shows "no posts" message | | Visit /posts/{id} for published post | Shows full post content | | Visit /posts/{id} for draft post | Returns 404 | | Switch language on posts page | Content displays in selected language | ## Definition of Done - [ ] Posts listing page works - [ ] Individual post page works - [ ] Only published posts visible - [ ] Reverse chronological order - [ ] Excerpts display correctly - [ ] Full content renders properly - [ ] Language-appropriate content shown - [ ] Mobile responsive - [ ] RTL support - [ ] Tests pass - [ ] Code formatted with Pint ## Dependencies - **Story 5.1:** Post creation (provides Post model with `published()` scope, `title`, `body`, `excerpt` accessors - see Database Schema and Post Model sections) - **Epic 1:** Base UI, navigation, bilingual infrastructure ## Estimation **Complexity:** Medium **Estimated Effort:** 3-4 hours --- ## Dev Agent Record ### Status **Ready for Review** ### Agent Model Used Claude Opus 4.5 ### Tasks Completed - [x] Add public posts routes to web.php (posts.index, posts.show) - [x] Create Posts Index Volt component with pagination - [x] Create Posts Show Volt component with 404 for unpublished - [x] Add prose-navy styling to app.css - [x] Add translation strings (read_more) to en/ar - [x] Write feature tests for public posts (13 tests, 32 assertions) - [x] Run Pint and verify all tests pass ### File List | File | Action | |------|--------| | `routes/web.php` | Modified - Added Volt routes for posts.index and posts.show | | `resources/views/livewire/pages/posts/index.blade.php` | Created - Posts listing Volt component | | `resources/views/livewire/pages/posts/show.blade.php` | Created - Individual post Volt component | | `resources/css/app.css` | Modified - Added prose-navy styling | | `lang/en/posts.php` | Modified - Added read_more translation | | `lang/ar/posts.php` | Modified - Added read_more translation | | `tests/Feature/Public/PostsTest.php` | Created - 13 feature tests | | `resources/views/pages/posts/index.blade.php` | Deleted - Replaced by Volt component | ### Change Log - Used `#[Layout('components.layouts.public')]` attribute for full-page Volt components - Used `Post::published()->latest()` for reverse chronological order - Used `$post->getTitle()`, `$post->getBody()`, `$post->getExcerpt()` methods for locale-aware content - Used `translatedFormat()` for locale-aware date formatting - Added `wire:navigate` for SPA-like navigation between pages ### Debug Log References None - implementation completed without issues. ### Completion Notes - All 13 public posts tests passing - Full test suite (362 tests, 883 assertions) passing - Code formatted with Pint --- ## QA Results ### Review Date: 2025-12-27 ### Reviewed By: Quinn (Test Architect) ### Code Quality Assessment **Overall: Excellent** - Implementation is clean, well-structured, and follows all project conventions. The class-based Volt components are properly implemented with appropriate use of Flux UI components, wire:navigate for SPA navigation, and wire:key for list items. The Post model's locale-aware methods (getTitle, getBody, getExcerpt) are used correctly throughout. **Strengths:** - Clean separation using published() scope - Proper 404 handling for unpublished posts using PostStatus enum - Locale-aware content rendering with fallback to Arabic - Professional prose-navy typography styling - Comprehensive test coverage (13 tests, 32 assertions) ### Refactoring Performed None required - implementation is solid and follows all coding standards. ### Compliance Check - Coding Standards: ✓ Class-based Volt pattern, Flux UI components, proper naming - Project Structure: ✓ Files in correct locations per source-tree.md - Testing Strategy: ✓ Pest tests with comprehensive coverage - All ACs Met: ✓ All 15 acceptance criteria verified ### Requirements Traceability | Acceptance Criteria | Test Coverage | |---------------------|---------------| | Public access (no login required) | ✓ `posts listing page is accessible without authentication` | | Reverse chronological order | ✓ `posts listing page displays in reverse chronological order` | | Post cards show title, date, excerpt, read more | ✓ `posts listing page shows post excerpt` | | Pagination | ✓ `posts listing page paginates results` | | Full post content displayed | ✓ `individual post page shows full content` | | Publication date shown | ✓ Verified in template | | Back to posts link | ✓ `individual post page shows back to posts link` | | Content in current locale | ✓ `posts listing page displays content in current locale` | | Individual post locale support | ✓ `individual post page displays content in current locale` | | Only published posts visible | ✓ `posts listing page shows only published posts` | | 404 for unpublished posts | ✓ `individual post page returns 404 for unpublished posts` | | 404 for non-existent posts | ✓ `individual post page returns 404 for non-existent posts` | | No posts message | ✓ `posts listing page shows no posts message when empty` | ### Improvements Checklist - [x] All acceptance criteria implemented and tested - [x] Bilingual support with locale-aware methods - [x] RTL support via public layout - [x] Professional typography with prose-navy - [x] Responsive design with max-width constraints - [x] SPA-like navigation with wire:navigate - [x] Proper pagination - [x] Code formatted with Pint ### Security Review **Status: PASS** - No security concerns identified. - Public read-only feature with no sensitive data exposure - Proper status check prevents accessing unpublished posts - No user input processed beyond route parameters - Route model binding handles non-existent posts safely ### Performance Considerations **Status: PASS** - No performance issues identified. - Simple queries with pagination (10 per page) - No N+1 query issues - Uses eager loading pattern via published() scope - CSS optimized with Tailwind ### Files Modified During Review None - no modifications required. ### Gate Status Gate: **PASS** → docs/qa/gates/5.4-public-posts-display.yml ### Recommended Status ✓ **Ready for Done** - All acceptance criteria met, comprehensive test coverage, no issues identified. Implementation follows all project conventions and coding standards.