15 KiB
Story 5.5: Post Search
Epic Reference
Epic 5: Posts/Blog System
User Story
As a website visitor, I want to search through blog posts, So that I can find relevant legal articles and information.
Story Context
Existing System Integration
- Integrates with: posts table, public posts listing
- Technology: Livewire Volt with real-time search
- Follows pattern: Search component pattern
- Touch points: Posts listing page
Acceptance Criteria
Search Functionality
- Search input on posts listing page
- Search by title (both languages)
- Search by body content (both languages)
- Real-time search results (debounced)
User Experience
- "No results found" message when empty
- Clear search button
- Search works in both Arabic and English
- Only searches published posts
Optional Enhancements
- Search highlights in results
- Search suggestions
Quality Requirements
- Debounce to reduce server load (300ms)
- Case-insensitive search
- Tests for search functionality
Technical Notes
Updated Posts Index Component
<?php
use App\Models\Post;
use Livewire\Volt\Component;
use Livewire\WithPagination;
new class extends Component {
use WithPagination;
public string $search = '';
public function updatedSearch()
{
$this->resetPage();
}
public function clearSearch(): void
{
$this->search = '';
$this->resetPage();
}
public function with(): array
{
return [
'posts' => Post::published()
->when($this->search, function ($query) {
$search = $this->search;
$query->where(function ($q) use ($search) {
$q->where('title_ar', 'like', "%{$search}%")
->orWhere('title_en', 'like', "%{$search}%")
->orWhere('body_ar', 'like', "%{$search}%")
->orWhere('body_en', 'like', "%{$search}%");
});
})
->latest()
->paginate(10),
];
}
}; ?>
<div class="max-w-4xl mx-auto">
<flux:heading>{{ __('posts.title') }}</flux:heading>
<!-- Search Bar -->
<div class="mt-6 relative">
<flux:input
wire:model.live.debounce.300ms="search"
placeholder="{{ __('posts.search_placeholder') }}"
class="w-full"
>
<x-slot:leading>
<flux:icon name="magnifying-glass" class="w-5 h-5 text-charcoal/50" />
</x-slot:leading>
</flux:input>
@if($search)
<button
wire:click="clearSearch"
class="absolute {{ app()->getLocale() === 'ar' ? 'left-3' : 'right-3' }} top-1/2 -translate-y-1/2 text-charcoal/50 hover:text-charcoal"
>
<flux:icon name="x-mark" class="w-5 h-5" />
</button>
@endif
</div>
<!-- Search Results Info -->
@if($search)
<p class="mt-4 text-sm text-charcoal/70">
@if($posts->total() > 0)
{{ __('posts.search_results', ['count' => $posts->total(), 'query' => $search]) }}
@else
{{ __('posts.no_results', ['query' => $search]) }}
@endif
</p>
@endif
<!-- Posts List -->
<div class="mt-8 space-y-6">
@forelse($posts as $post)
<article class="bg-cream p-6 rounded-lg shadow-sm hover:shadow-md transition-shadow">
<h2 class="text-xl font-semibold text-navy">
<a href="{{ route('posts.show', $post) }}" class="hover:text-gold">
@if($search)
{!! $this->highlightSearch($post->title, $search) !!}
@else
{{ $post->title }}
@endif
</a>
</h2>
<time class="text-sm text-charcoal/70 mt-2 block">
{{ $post->created_at->translatedFormat('d F Y') }}
</time>
<p class="mt-3 text-charcoal">
@if($search)
{!! $this->highlightSearch($post->excerpt, $search) !!}
@else
{{ $post->excerpt }}
@endif
</p>
<a href="{{ route('posts.show', $post) }}" class="text-gold hover:underline mt-4 inline-block">
{{ __('posts.read_more') }} →
</a>
</article>
@empty
<div class="text-center py-12">
@if($search)
<flux:icon name="magnifying-glass" class="w-12 h-12 text-charcoal/30 mx-auto mb-4" />
<p class="text-charcoal/70">{{ __('posts.no_results', ['query' => $search]) }}</p>
<flux:button wire:click="clearSearch" class="mt-4">
{{ __('posts.clear_search') }}
</flux:button>
@else
<p class="text-charcoal/70">{{ __('posts.no_posts') }}</p>
@endif
</div>
@endforelse
</div>
{{ $posts->links() }}
</div>
Highlight Helper Method
public function highlightSearch(string $text, string $search): string
{
if (empty($search)) {
return e($text);
}
$escapedText = e($text);
$escapedSearch = e($search);
return preg_replace(
'/(' . preg_quote($escapedSearch, '/') . ')/iu',
'<mark class="bg-gold/30 rounded px-1">$1</mark>',
$escapedText
);
}
Translation Strings
// resources/lang/en/posts.php
return [
'search_placeholder' => 'Search articles...',
'search_results' => 'Found :count results for ":query"',
'no_results' => 'No results found for ":query"',
'clear_search' => 'Clear search',
];
// resources/lang/ar/posts.php
return [
'search_placeholder' => 'البحث في المقالات...',
'search_results' => 'تم العثور على :count نتيجة لـ ":query"',
'no_results' => 'لم يتم العثور على نتائج لـ ":query"',
'clear_search' => 'مسح البحث',
];
Testing
it('searches posts by title', function () {
Post::factory()->published()->create(['title_en' => 'Legal Rights Guide']);
Post::factory()->published()->create(['title_en' => 'Tax Information']);
Volt::test('posts.index')
->set('search', 'Legal')
->assertSee('Legal Rights Guide')
->assertDontSee('Tax Information');
});
it('searches posts by body content', function () {
Post::factory()->published()->create([
'title_en' => 'Post 1',
'body_en' => 'This discusses property law topics.',
]);
Post::factory()->published()->create([
'title_en' => 'Post 2',
'body_en' => 'This is about family matters.',
]);
Volt::test('posts.index')
->set('search', 'property')
->assertSee('Post 1')
->assertDontSee('Post 2');
});
it('shows no results message', function () {
Volt::test('posts.index')
->set('search', 'nonexistent')
->assertSee('No results found');
});
it('only searches published posts', function () {
Post::factory()->create(['status' => 'draft', 'title_en' => 'Draft Post']);
Post::factory()->published()->create(['title_en' => 'Published Post']);
Volt::test('posts.index')
->set('search', 'Post')
->assertSee('Published Post')
->assertDontSee('Draft Post');
});
Definition of Done
- Search input on posts page
- Searches title in both languages
- Searches body in both languages
- Real-time results with debounce
- Clear search button works
- "No results" message shows
- Only published posts searched
- Search highlighting works
- Tests pass
- Code formatted with Pint
Dev Agent Record
Agent Model Used
Claude Opus 4.5
Completion Notes
- Implemented search functionality on the posts index page with real-time debounced search (300ms)
- Search queries both title and body fields as JSON columns (works with SQLite for tests and MySQL/MariaDB for production)
- Added search result highlighting with gold background on matched terms
- Clear search button with RTL/LTR position awareness
- Added bilingual translation strings for search UI (English and Arabic)
- 9 new search-specific tests added to PostsTest.php covering title search, body search, Arabic search, case-insensitivity, pagination reset, and clear functionality
- All 22 posts tests pass, full test suite passes
File List
resources/views/livewire/pages/posts/index.blade.php(modified) - Added search functionality and highlight methodlang/en/posts.php(modified) - Added search translation stringslang/ar/posts.php(modified) - Added Arabic search translation stringstests/Feature/Public/PostsTest.php(modified) - Added 9 search tests
Change Log
- Added
$searchproperty with debounced live binding - Added
updatedSearch()method to reset pagination on search change - Added
clearSearch()method to clear search and reset pagination - Added
highlightSearch()method for search term highlighting with HTML escaping - Updated
with()query to filter posts by title and body columns using LIKE - Added search input with magnifying glass icon
- Added clear search button (X) with RTL-aware positioning
- Added search results info message showing count or "no results" message
- Added empty state with search icon and clear button when search yields no results
- Search highlighting applied to both title and excerpt in search results
Dependencies
- Story 5.4: Public posts display
Estimation
Complexity: Low-Medium Estimated Effort: 2-3 hours
QA Results
Review Date: 2025-12-27
Reviewed By: Quinn (Test Architect)
Code Quality Assessment
The implementation is solid and follows project conventions well. The search functionality is implemented correctly using Livewire Volt's class-based pattern with proper debouncing, pagination reset, and bilingual support. The highlightSearch() method is well-designed with proper XSS protection using e() for both input text and search term before applying regex highlighting.
Notable positives:
- Clean, readable code following coding standards
- Proper use of Flux UI components
- RTL-aware positioning for the clear search button
- Good separation between display logic (getTitle/getExcerpt) and search logic
Refactoring Performed
None required - the implementation is clean and follows best practices.
Compliance Check
- Coding Standards: ✓ Uses class-based Volt pattern, Flux UI components, proper testing
- Project Structure: ✓ Files in correct locations, naming follows conventions
- Testing Strategy: ✓ 9 comprehensive tests with 22 assertions covering all scenarios
- All ACs Met: ✓ All acceptance criteria verified with corresponding tests
Improvements Checklist
- Search functionality implemented with proper debouncing (300ms)
- Bilingual search working for both title and body
- XSS protection in highlight function
- Case-insensitive search implemented
- Clear search resets pagination
- "No results" message displays correctly
- Only published posts are searched
- Optional: Consider JSON path operators (
->) for more precise JSON column searches in production MySQL (current LIKE on JSON works but is less precise) - Optional enhancement: Search suggestions (marked as optional in story, not implemented)
Security Review
✓ XSS Protection: The highlightSearch() method properly escapes HTML using e() before applying regex replacement, preventing XSS attacks.
✓ Regex Injection: preg_quote() is used to escape special regex characters in the search term.
✓ Authorization: Search is scoped to published() posts only - draft content is not accessible.
Performance Considerations
✓ Debouncing: 300ms debounce reduces unnecessary server requests ✓ Pagination: 10 items per page limits data transfer
- Note: LIKE queries on JSON columns may become slow with very large datasets. For a blog with hundreds/thousands of posts, this is acceptable. For larger scale, consider full-text search indexes.
Files Modified During Review
None - no refactoring was required.
Gate Status
Gate: PASS → docs/qa/gates/5.5-post-search.yml
Recommended Status
✓ Ready for Done - All acceptance criteria met, tests pass, code follows standards.
Story DoD Checklist
1. Requirements Met:
- All functional requirements specified in the story are implemented.
- Search input on posts listing page ✓
- Search by title (both languages) ✓
- Search by body content (both languages) ✓
- Real-time search with 300ms debounce ✓
- All acceptance criteria defined in the story are met.
- "No results found" message ✓
- Clear search button ✓
- Works in Arabic and English ✓
- Only searches published posts ✓
- Search highlights in results ✓
2. Coding Standards & Project Structure:
- All new/modified code strictly adheres to
Operational Guidelines. - All new/modified code aligns with
Project Structure(file locations, naming, etc.). - Adherence to
Tech Stackfor technologies/versions used. - [N/A] Adherence to
Api ReferenceandData Models- No API or data model changes. - Basic security best practices applied (HTML escaping in highlight function).
- No new linter errors or warnings introduced (Pint passes).
- Code is well-commented where necessary.
3. Testing:
- All required unit tests implemented (9 new search tests).
- [N/A] Integration tests - Component tests cover functionality adequately.
- All tests pass successfully (22 posts tests, full suite passes).
- Test coverage meets project standards.
4. Functionality & Verification:
- Functionality verified through comprehensive test suite.
- Edge cases handled (empty search, no results, case-insensitivity, RTL support).
5. Story Administration:
- All tasks within the story file are marked as complete.
- Decisions documented (search on raw JSON columns vs JSON path, highlight implementation).
- Story wrap up section completed with change log and file list.
6. Dependencies, Build & Configuration:
- Project builds successfully without errors.
- Project linting passes (Pint --dirty).
- [N/A] No new dependencies added.
- [N/A] No new environment variables or configurations.
7. Documentation:
- Inline code documentation adequate (method names are self-documenting).
- [N/A] No user-facing documentation changes needed.
- [N/A] No architectural changes made.
Final Confirmation
- I, the Developer Agent, confirm that all applicable items above have been addressed.
Status: Ready for Review