Sitecore SearchPageView Component
SearchPageView Component
TL;DR: A zero-UI React component that automatically tracks page views and sends analytics events to Sitecore Search Discovery API, enabling personalized search experiences and content recommendations.
🎯 What Problem Does This Solve?
Sitecore Search uses behavioral signals to power intelligent search results and content recommendations. Without page view tracking, you’re missing a critical piece of the personalization puzzle.
The SearchPageView component bridges this gap by:
- Tracking content engagement — Every page view feeds into Sitecore’s ML models
- Enabling personalization — Users get search results tailored to their browsing history
- Supporting analytics — Build content performance dashboards from real user behavior
- Maintaining user identity — Persistent cookies ensure cross-session tracking
🏗️ Architecture Overview
flowchart TD
subgraph Component["SearchPageView"]
A["useEffect (on mount)"] --> B["Build Entity"]
B --> C["entityPageView()<br/>Creates event payload"]
C --> D["publishEvent()<br/>POST to Discovery API"]
E["Next.js Router"] --> F["Extract Locale"]
end
D --> G["Sitecore Discovery API<br/>discover.sitecorecloud.io/event/..."]
🔧 How It Works
Step 1: Component Mounts
When the component mounts, a useEffect hook with an empty dependency array triggers exactly once per page load.
useEffect(() => {
triggerPageView();
}, []);
Step 2: Build the Entity Object
The component constructs an entity object containing:
| Field | Source | Example |
|---|---|---|
entity_type | Static | "content" |
id | Canonical URL (sanitized) | "https___example_com_en-us_products" |
title | document.title | "Products | My Site" |
uri | window.location.href | "https://example.com/en-us/products" |
Step 3: Generate Canonical ID
URLs are transformed into Sitecore-safe identifiers:
// Input: https://example.com/en-us/products/camera?ref=home
// Output: https___example_com_en-us_products_camera_ref_home
const id = `${window.location.href}`
.replaceAll("/", "_")
.replaceAll(".", "_")
.replaceAll(":", "_")
.replace(/(ja-jp|zh-cn)(_|$|\/)/g, "") // Strip CJK locales for indexing
.replaceAll(/[^a-zA-Z\d_-]/g, "_"); // Sanitize remaining characters
Why strip Asian locales? This implementation normalizes CJK (Chinese, Japanese, Korean) locale prefixes to prevent duplicate content entries in the search index when the same content exists across locales.
Step 4: Extract Locale Information
The Next.js router provides the current locale (e.g., en-us), which is split into country and language codes:
const { locale } = useRouter();
// locale: "en-us" → country: "US", language: "en"
const country = locale?.split("-")[1] || "US";
const language = locale?.split("-")[0] || "en";
Step 5: Publish to Discovery API
The publishEvent function sends the payload to Sitecore’s event ingestion endpoint:
POST https://discover.sitecorecloud.io/event/{CUSTOMER_KEY}/v4/publish
📦 Event Payload Structure
Here’s the complete payload sent to the Discovery API:
{
name: "entity_page",
action: "view",
uuid: "domain123-ab-cd-4x-1p-xxxx-yyyy-zzzz-1703520000000",
client_time_ms: 1703520000000,
value: {
context: {
page: {
uri: "/en-us/products"
},
user: {
cuid: "domain123-ab-cd-4x-1p-xxxx-yyyy-zzzz-1703520000000"
},
geo: {
ip: "203.0.113.42"
},
browser: {
user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)..."
},
locale: {
country: "US",
language: "en"
}
},
entities: [
{
entity_type: "content",
id: "https___example_com_en-us_products",
title: "Products | My Site",
uri: "https://example.com/en-us/products"
}
]
}
}
🚀 Quick Start
Basic Usage
Drop the component anywhere in your layout — it renders nothing visually:
import SearchPageView from "src/components/search/searchPageView";
const Layout = ({ children }) => {
return (
<>
<SearchPageView />
<Header />
<main>{children}</main>
<Footer />
</>
);
};
Next.js App Router Example
For App Router projects, include in your root layout:
// app/layout.tsx
import SearchPageView from "src/components/search/searchPageView";
export default function RootLayout({ children }) {
return (
<html>
<body>
<SearchPageView />
{children}
</body>
</html>
);
}
Conditional Tracking
Exclude certain pages (e.g., admin routes) from tracking:
import { useRouter } from "next/router";
import SearchPageView from "src/components/search/searchPageView";
const Layout = ({ children }) => {
const { pathname } = useRouter();
const shouldTrack = !pathname.startsWith("/admin");
return (
<>
{shouldTrack && <SearchPageView />}
{children}
</>
);
};
⚙️ Configuration
Environment Variables
| Variable | Required | Description |
|---|---|---|
NEXT_PUBLIC_SEARCH_CUSTOMER_KEY | ✅ | Your Sitecore Search customer identifier (e.g., 123-abc) |
NEXT_PUBLIC_SEARCH_API_KEY | ✅ | Authorization token for the Discovery API |
Add these to your .env.local:
NEXT_PUBLIC_SEARCH_CUSTOMER_KEY=your-customer-key
NEXT_PUBLIC_SEARCH_API_KEY=your-api-key
🧩 Dependencies
This component relies on helper functions from src/helpers/sitecore-search-analytics.ts:
| Function | Purpose |
|---|---|
entityPageView | Constructs the complete event payload with context and entities |
publishEvent | POSTs the event to Sitecore Discovery API |
context | Builds the context object (page, user, geo, browser, locale) |
getDomainId | Retrieves or generates persistent user ID (__ruid cookie) |
getIp | Fetches user’s IP address via ipify.org (cached in cookie) |
🔐 User Identity Management
How User IDs Work
The component uses a persistent cookie (__ruid) to maintain user identity:
__ruid = {domainId}-{randomUUID}-{timestamp}
Example: abc123-xy-ab-4x-1p-kj2h-mn3i-op4q-1703520000000
- Domain ID: Extracted from your
NEXT_PUBLIC_SEARCH_CUSTOMER_KEY - UUID: Randomly generated identifier
- Timestamp: Creation time in milliseconds
Cookie Lifecycle
| Cookie | Purpose | Expiration |
|---|---|---|
__ruid | User identity | 1 day |
currentIp | Cached IP address | 1 day |
Privacy Note: Consider extending cookie duration based on your analytics needs and privacy policy requirements.
⚠️ Important Considerations
Client-Side Only
This component uses browser APIs (window, document, navigator) and must run client-side. It won’t work during server-side rendering.
// ❌ Won't work in getServerSideProps or Server Components
// ✅ Works in useEffect, client components, or pages
Single Fire Per Mount
The empty dependency array ensures the event fires exactly once per page navigation:
useEffect(() => {
triggerPageView();
}, []); // Empty array = runs once on mount
For SPAs, this means a new event fires on each route change (component remount).
Silent Error Handling
Network failures are currently unhandled. For production monitoring, consider adding:
try {
await publishEvent(eventPayload);
} catch (error) {
console.error("[SearchPageView] Failed to publish event:", error);
// Optionally send to your error tracking service
}
IP Detection Latency
The component fetches the user’s IP from api.ipify.org. This external call:
- Adds ~100-300ms latency on first load
- Is cached in a cookie for subsequent pages
- May fail in restricted network environments
🧪 Testing & Debugging
Verify Events in Browser DevTools
- Open Network tab
- Filter by
discover.sitecorecloud.io - Navigate to a page
- Confirm a POST request with status
200
Check Request Payload
// In browser console after page load
document.cookie.match(/__ruid=([^;]+)/)?.[1];
// Should return your user ID
Debug Mode
Add temporary logging to verify the flow:
const triggerPageView = async () => {
console.log("[SearchPageView] Firing page view...");
const payload = await entityPageView(/* ... */);
console.log("[SearchPageView] Payload:", payload);
await publishEvent(payload);
console.log("[SearchPageView] Event published successfully");
};
📚 Related Resources
📄 Source Code
import {
entityPageView,
publishEvent,
} from "src/helpers/sitecore-search-analytics";
import { JSX, useEffect } from "react";
import { useRouter } from "next/router";
const SearchPageView = (): JSX.Element => {
const { locale } = useRouter();
const triggerPageView = async () => {
const { title } = document;
// Generate canonical URL ID
const id = `${window.location.href}`
.replaceAll("/", "_")
.replaceAll(".", "_")
.replaceAll(":", "_")
.replace(/(ja-jp|zh-cn)(_|$|\/)/g, "")
.replaceAll(/[^a-zA-Z\d_-]/g, "_");
await publishEvent(
await entityPageView(
[
{
entity_type: "content",
id,
title,
uri: window.location.href,
},
],
locale?.split("-")[1] || "US",
locale?.split("-")[0] || "en"
)
);
};
useEffect(() => {
triggerPageView();
}, []);
return <></>;
};
export default SearchPageView;
Happy Tracking! 🚀
Questions? Contact me on LinkedIn.