Skip to main content
arrow_back Back to Blog
Sitecore Search

Sitecore SearchPageView Component

15 min read

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:

FieldSourceExample
entity_typeStatic"content"
idCanonical URL (sanitized)"https___example_com_en-us_products"
titledocument.title"Products | My Site"
uriwindow.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

VariableRequiredDescription
NEXT_PUBLIC_SEARCH_CUSTOMER_KEYYour Sitecore Search customer identifier (e.g., 123-abc)
NEXT_PUBLIC_SEARCH_API_KEYAuthorization 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:

FunctionPurpose
entityPageViewConstructs the complete event payload with context and entities
publishEventPOSTs the event to Sitecore Discovery API
contextBuilds the context object (page, user, geo, browser, locale)
getDomainIdRetrieves or generates persistent user ID (__ruid cookie)
getIpFetches 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
CookiePurposeExpiration
__ruidUser identity1 day
currentIpCached IP address1 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

  1. Open Network tab
  2. Filter by discover.sitecorecloud.io
  3. Navigate to a page
  4. 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");
};


📄 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.