Sitecore Headless Search Implementation Guide - ysolution-it/sitecore-wiki GitHub Wiki
This guide provides a complete implementation for creating a search page in a Sitecore headless portal. The solution includes both backend API and frontend React components with full search functionality.
The search implementation consists of:
- Backend API: C# controller that handles search requests and integrates with Sitecore's search capabilities
- Frontend Component: React-based search interface with filtering, pagination, and responsive design
- Search Features: Full-text search, content type filtering, pagination, and error handling
Create a new API controller to handle search requests:
// Controllers/SearchController.cs
using Microsoft.AspNetCore.Mvc;
using Sitecore.AspNet.RenderingEngine.Configuration;
using Sitecore.AspNet.RenderingEngine.Services;
using Sitecore.LayoutService.Client.Exceptions;
using Sitecore.LayoutService.Client.Interfaces;
using System.Threading.Tasks;
[ApiController]
[Route("api/[controller]")]
public class SearchController : ControllerBase
{
private readonly ILayoutServiceClient _layoutServiceClient;
private readonly RenderingEngineOptions _renderingEngineOptions;
public SearchController(
ILayoutServiceClient layoutServiceClient,
RenderingEngineOptions renderingEngineOptions)
{
_layoutServiceClient = layoutServiceClient;
_renderingEngineOptions = renderingEngineOptions;
}
[HttpGet]
public async Task<IActionResult> Search(
[FromQuery] string query,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 10,
[FromQuery] string[] contentTypes = null,
[FromQuery] string language = "en")
{
try
{
if (string.IsNullOrWhiteSpace(query))
{
return BadRequest("Search query is required");
}
// Build search parameters
var searchParams = new Dictionary<string, object>
{
["query"] = query,
["page"] = page,
["pageSize"] = pageSize,
["language"] = language
};
if (contentTypes != null && contentTypes.Length > 0)
{
searchParams["contentTypes"] = contentTypes;
}
// Call Sitecore search endpoint
var searchResults = await PerformSearch(searchParams);
return Ok(searchResults);
}
catch (Exception ex)
{
return StatusCode(500, new { message = "Search failed", error = ex.Message });
}
}
private async Task<object> PerformSearch(Dictionary<string, object> searchParams)
{
// Option 1: Using Sitecore Search (recommended)
// This assumes you have Sitecore Search configured
var searchQuery = BuildSitecoreSearchQuery(searchParams);
// Option 2: Using GraphQL
// var graphQLQuery = BuildGraphQLQuery(searchParams);
// var result = await _layoutServiceClient.Request(graphQLQuery);
// Option 3: Using Content Search API
// var contentSearchQuery = BuildContentSearchQuery(searchParams);
// Mock response - replace with actual implementation
return new
{
Results = new[]
{
new
{
Id = "item1",
Title = "Sample Article",
Content = "This is sample content...",
Url = "/articles/sample-article",
ContentType = "Article",
LastModified = DateTime.UtcNow.AddDays(-5)
}
},
TotalCount = 1,
Page = (int)searchParams["page"],
PageSize = (int)searchParams["pageSize"],
TotalPages = 1
};
}
private string BuildSitecoreSearchQuery(Dictionary<string, object> searchParams)
{
// Build your Sitecore Search query here
// This will depend on your specific Sitecore Search configuration
return $"q={searchParams["query"]}&rows={searchParams["pageSize"]}&start={(((int)searchParams["page"] - 1) * (int)searchParams["pageSize"])}";
}
}
Key Components:
-
Dependency Injection: Uses
ILayoutServiceClient
for Sitecore integration - Search Parameters: Accepts query, pagination, content type filters, and language
- Flexible Search Methods: Supports multiple Sitecore search approaches
- Error Handling: Proper HTTP response codes and error messages
- Pagination Support: Built-in pagination with configurable page sizes
Search Integration Options:
- Sitecore Search: Modern cloud-based search service (recommended)
- GraphQL: Query Sitecore content via GraphQL endpoints
- Content Search API: Traditional Sitecore search using Lucene/Solr
Create a comprehensive search component with modern UI:
// components/SearchPage.jsx
import React, { useState, useEffect, useCallback } from 'react';
import { Search, Filter, ChevronLeft, ChevronRight, Clock, FileText } from 'lucide-react';
const SearchPage = () => {
const [searchQuery, setSearchQuery] = useState('');
const [searchResults, setSearchResults] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [totalResults, setTotalResults] = useState(0);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize] = useState(10);
const [contentTypeFilter, setContentTypeFilter] = useState([]);
const [error, setError] = useState(null);
const contentTypes = [
{ value: 'Article', label: 'Articles' },
{ value: 'Page', label: 'Pages' },
{ value: 'News', label: 'News' },
{ value: 'Event', label: 'Events' },
{ value: 'Product', label: 'Products' }
];
const performSearch = useCallback(async (query, page = 1, filters = []) => {
if (!query.trim()) {
setSearchResults([]);
setTotalResults(0);
return;
}
setIsLoading(true);
setError(null);
try {
const params = new URLSearchParams({
query: query.trim(),
page: page.toString(),
pageSize: pageSize.toString()
});
if (filters.length > 0) {
filters.forEach(filter => params.append('contentTypes', filter));
}
const response = await fetch(`/api/search?${params}`);
if (!response.ok) {
throw new Error(`Search failed: ${response.statusText}`);
}
const data = await response.json();
setSearchResults(data.Results || []);
setTotalResults(data.TotalCount || 0);
setCurrentPage(data.Page || 1);
} catch (err) {
setError(err.message);
setSearchResults([]);
setTotalResults(0);
} finally {
setIsLoading(false);
}
}, [pageSize]);
const handleSearch = (e) => {
e.preventDefault();
setCurrentPage(1);
performSearch(searchQuery, 1, contentTypeFilter);
};
const handlePageChange = (newPage) => {
setCurrentPage(newPage);
performSearch(searchQuery, newPage, contentTypeFilter);
};
const handleFilterChange = (contentType) => {
const newFilters = contentTypeFilter.includes(contentType)
? contentTypeFilter.filter(f => f !== contentType)
: [...contentTypeFilter, contentType];
setContentTypeFilter(newFilters);
setCurrentPage(1);
performSearch(searchQuery, 1, newFilters);
};
const totalPages = Math.ceil(totalResults / pageSize);
return (
<div className="max-w-6xl mx-auto p-6">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-6">Search Portal</h1>
{/* Search Form */}
<form onSubmit={handleSearch} className="mb-6">
<div className="relative">
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search for content..."
className="w-full pl-12 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
<Search className="absolute left-4 top-3.5 h-5 w-5 text-gray-400" />
<button
type="submit"
className="absolute right-3 top-2 px-4 py-1.5 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
Search
</button>
</div>
</form>
{/* Content Type Filters */}
<div className="mb-6">
<div className="flex items-center gap-2 mb-3">
<Filter className="h-5 w-5 text-gray-600" />
<span className="font-medium text-gray-700">Filter by content type:</span>
</div>
<div className="flex flex-wrap gap-2">
{contentTypes.map(type => (
<button
key={type.value}
onClick={() => handleFilterChange(type.value)}
className={`px-3 py-1 rounded-full text-sm font-medium transition-colors ${
contentTypeFilter.includes(type.value)
? 'bg-blue-600 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
}`}
>
{type.label}
</button>
))}
</div>
</div>
</div>
{/* Search Results */}
{isLoading && (
<div className="text-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<p className="text-gray-600 mt-4">Searching...</p>
</div>
)}
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
<p className="text-red-800">Error: {error}</p>
</div>
)}
{!isLoading && !error && searchQuery && (
<div className="mb-6">
<p className="text-gray-600">
Found {totalResults} results for "{searchQuery}"
{contentTypeFilter.length > 0 && (
<span className="ml-2">
(filtered by: {contentTypeFilter.join(', ')})
</span>
)}
</p>
</div>
)}
{/* Results List */}
{searchResults.length > 0 && (
<div className="space-y-4">
{searchResults.map((result, index) => (
<div key={result.Id || index} className="bg-white border border-gray-200 rounded-lg p-6 hover:shadow-md transition-shadow">
<div className="flex items-start justify-between">
<div className="flex-1">
<h3 className="text-xl font-semibold text-blue-600 hover:text-blue-800 mb-2">
<a href={result.Url} className="hover:underline">
{result.Title}
</a>
</h3>
<p className="text-gray-700 mb-3 line-clamp-2">
{result.Content}
</p>
<div className="flex items-center gap-4 text-sm text-gray-500">
<span className="flex items-center gap-1">
<FileText className="h-4 w-4" />
{result.ContentType}
</span>
<span className="flex items-center gap-1">
<Clock className="h-4 w-4" />
{new Date(result.LastModified).toLocaleDateString()}
</span>
</div>
</div>
</div>
</div>
))}
</div>
)}
{/* Pagination */}
{totalPages > 1 && (
<div className="flex items-center justify-center gap-2 mt-8">
<button
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
className="flex items-center gap-1 px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
<ChevronLeft className="h-4 w-4" />
Previous
</button>
{[...Array(Math.min(5, totalPages))].map((_, i) => {
const page = i + 1;
return (
<button
key={page}
onClick={() => handlePageChange(page)}
className={`px-3 py-2 text-sm font-medium rounded-lg ${
currentPage === page
? 'bg-blue-600 text-white'
: 'text-gray-500 bg-white border border-gray-300 hover:bg-gray-50'
}`}
>
{page}
</button>
);
})}
<button
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
className="flex items-center gap-1 px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Next
<ChevronRight className="h-4 w-4" />
</button>
</div>
)}
{/* No Results */}
{!isLoading && !error && searchQuery && searchResults.length === 0 && (
<div className="text-center py-12">
<p className="text-gray-600 text-lg">No results found for "{searchQuery}"</p>
<p className="text-gray-500 mt-2">Try adjusting your search terms or filters</p>
</div>
)}
</div>
);
};
export default SearchPage;
State Management:
-
searchQuery
: Current search term -
searchResults
: Array of search results -
isLoading
: Loading state indicator -
totalResults
: Total number of results for pagination -
currentPage
: Current page number -
contentTypeFilter
: Active content type filters -
error
: Error state for error handling
Key Features:
-
Search Form:
- Controlled input with search icon
- Form submission handling
- Inline search button
-
Content Type Filtering:
- Toggle-style filter buttons
- Multiple filter selection
- Automatic search re-execution on filter change
-
Results Display:
- Card-based layout for each result
- Displays title, content preview, type, and date
- Clickable titles linking to content
-
Pagination:
- Previous/Next navigation
- Page number buttons
- Responsive pagination display
-
Loading States:
- Spinner animation during search
- Error message display
- Empty state handling
- Add the SearchController to your Sitecore headless application
-
Configure Dependencies in your
Startup.cs
orProgram.cs
-
Set up Search Integration:
- For Sitecore Search: Configure search service connection
- For GraphQL: Set up GraphQL endpoint
- For Content Search: Configure search indexes
-
Install Dependencies:
npm install lucide-react
-
Add the SearchPage Component to your React application
-
Configure Routing to include the search page
-
Customize Content Types to match your portal structure
// Add to your search method
private async Task<object> PerformSitecoreSearch(Dictionary<string, object> searchParams)
{
var searchClient = new SearchClient(/* your config */);
var searchRequest = new SearchRequest
{
Query = searchParams["query"].ToString(),
Rows = (int)searchParams["pageSize"],
Start = ((int)searchParams["page"] - 1) * (int)searchParams["pageSize"]
};
var response = await searchClient.SearchAsync(searchRequest);
return response;
}
// GraphQL query example
private string BuildGraphQLQuery(Dictionary<string, object> searchParams)
{
return $@"
{{
search(
query: ""{searchParams["query"]}""
first: {searchParams["pageSize"]}
after: ""{((int)searchParams["page"] - 1) * (int)searchParams["pageSize"]}""
) {{
results {{
id
name
url
fields {{
name
value
}}
}}
total
}}
}}";
}
-
Autocomplete/Suggestions:
- Add debounced search as user types
- Implement search suggestions dropdown
-
Faceted Search:
- Add category-based filtering
- Date range filters
- Author filtering
-
Search Analytics:
- Track search queries
- Monitor search performance
- Analyze popular content
-
Caching:
- Implement result caching
- Use Redis for distributed caching
-
Indexing:
- Optimize Sitecore search indexes
- Regular index rebuilding
-
Frontend Optimization:
- Implement virtual scrolling for large result sets
- Add result caching with React Query
The component uses Tailwind CSS classes that can be customized:
// Custom theme colors
const searchButtonClass = "bg-your-primary-color hover:bg-your-primary-dark";
const filterButtonClass = "bg-your-secondary-color text-white";
Modify the contentTypes
array to match your Sitecore templates:
const contentTypes = [
{ value: 'YourTemplate1', label: 'Custom Content Type 1' },
{ value: 'YourTemplate2', label: 'Custom Content Type 2' },
// Add your specific content types
];
Customize the result display based on your content structure:
// Custom result rendering
const renderResult = (result) => (
<div className="custom-result-layout">
<h3>{result.Title}</h3>
<p>{result.Description}</p>
<div className="custom-metadata">
<span>{result.Author}</span>
<span>{result.Category}</span>
</div>
</div>
);
-
Search Not Working:
- Check API endpoint configuration
- Verify search service connectivity
- Ensure proper CORS settings
-
No Results Returned:
- Verify search indexes are populated
- Check content permissions
- Validate search query syntax
-
Performance Issues:
- Optimize search indexes
- Implement proper caching
- Use pagination effectively
-
Enable API Logging:
// Add logging to your search controller _logger.LogInformation($"Search query: {query}, Results: {results.Count}");
-
Frontend Debugging:
// Add console logging console.log('Search params:', { query, page, filters }); console.log('Search results:', searchResults);
This implementation provides a comprehensive search solution for your Sitecore headless portal. The modular design allows for easy customization and extension based on your specific requirements. The combination of a robust backend API and a modern React frontend ensures a smooth user experience while maintaining scalability and performance.
The solution supports multiple search approaches, making it flexible enough to work with different Sitecore configurations and search requirements. Regular updates and optimizations will help maintain search performance as your content grows.