Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(headless SSR): add NavigatorContext in SSR sample #4238

Merged
merged 43 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
9ac3b3e
add CoreEngineNext type
y-lakhdar Jul 15, 2024
da8d657
draft commerce engine
y-lakhdar Jul 15, 2024
9dfd822
add commerce engine
y-lakhdar Jul 17, 2024
9b55110
revert CommerceEngineOptions modification
y-lakhdar Jul 17, 2024
1eec820
Merge branch 'master' of github.com:coveo/ui-kit into KIT-3390
y-lakhdar Jul 17, 2024
4335428
convert type to interface
y-lakhdar Jul 17, 2024
5011f94
remove legacySearchAction type
y-lakhdar Jul 23, 2024
c0d257c
remove search type support
y-lakhdar Jul 25, 2024
0d69866
add ssr.index.ts
y-lakhdar Jul 25, 2024
5c63b4e
add sample
y-lakhdar Jul 25, 2024
eed1bfd
draft
y-lakhdar Jul 25, 2024
ec998c1
revert samples
y-lakhdar Jul 25, 2024
68ea3c7
remove ssr sample
y-lakhdar Jul 25, 2024
8fc01df
clean PR
y-lakhdar Jul 25, 2024
1abd6b5
mixed reset
y-lakhdar Jul 25, 2024
045963a
clean PR
y-lakhdar Jul 25, 2024
ca34fe8
update readme
y-lakhdar Jul 25, 2024
2d5ddf3
add navigator context provider
y-lakhdar Jul 25, 2024
99593c6
log warning when context provider is missing
y-lakhdar Jul 26, 2024
243b35b
add nextjs implementation of navigator context
y-lakhdar Jul 26, 2024
b0f23c7
add navigator context doc
y-lakhdar Jul 26, 2024
71c825a
remove hard coded client id fallback
y-lakhdar Jul 26, 2024
5aab898
rename const
y-lakhdar Jul 26, 2024
455cde8
renaming vars
y-lakhdar Jul 26, 2024
6a336ec
add NavigatorContext in ssr sample
y-lakhdar Jul 30, 2024
eea5fc9
Merge branch 'master' of github.com:coveo/ui-kit into KIT-3395
y-lakhdar Aug 1, 2024
d61812e
lock
y-lakhdar Aug 1, 2024
84de638
fix merge conflicts
y-lakhdar Aug 1, 2024
bd17441
revert merge concfix merge conflicts
y-lakhdar Aug 1, 2024
f8254c1
delete unecessary file
y-lakhdar Aug 1, 2024
66ec80a
lock
y-lakhdar Aug 1, 2024
43f0930
add doc to Navigator Context
y-lakhdar Aug 1, 2024
462cba8
fix build:doc
y-lakhdar Aug 1, 2024
26ea809
Merge branch 'KIT-3395' of github.com:coveo/ui-kit into KIT-3116
y-lakhdar Aug 1, 2024
f59533b
apply corrections
y-lakhdar Aug 1, 2024
6748d58
Merge branch 'master' into KIT-3116
y-lakhdar Aug 5, 2024
198691a
update page-router sample
y-lakhdar Aug 5, 2024
7f50b51
Merge branch 'KIT-3116' of github.com:coveo/ui-kit into KIT-3116
y-lakhdar Aug 5, 2024
0bb2483
Merge branch 'master' into KIT-3116
alexprudhomme Aug 5, 2024
7a1a40c
Merge branch 'master' into KIT-3116
alexprudhomme Aug 7, 2024
077002d
fix pages-router by splitting contextProvider in two different strate…
alexprudhomme Aug 8, 2024
6e13c9f
Merge branch 'master' into KIT-3116
y-lakhdar Aug 8, 2024
222a94b
Merge branch 'KIT-3116' of https://github.com/coveo/ui-kit into KIT-3116
alexprudhomme Aug 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/headless/src/ssr.index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export type {
InferBuildResult,
} from './app/ssr-engine/types/core-engine';
export type {LoggerOptions} from './app/logger';
export type {NavigatorContext} from './app/navigatorContextProvider';

export type {LogLevel} from './app/logger';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import SearchPage from '@/common/components/generic/search-page';
import {fetchStaticState} from '@/common/lib/generic/engine';
import {
fetchStaticState,
setNavigatorContextProvider,
} from '@/common/lib/generic/engine';
import {buildSSRSearchParameterSerializer} from '@coveo/headless/ssr';
import {headers} from 'next/headers';
import {NextJsAppRouterNavigatorContext} from '../../navigatorContextProvider';

/**
* This file defines a Search component that uses the Coveo Headless library to manage its state.
Expand All @@ -16,12 +21,22 @@ import {buildSSRSearchParameterSerializer} from '@coveo/headless/ssr';
export default async function Search(url: {
searchParams: {[key: string]: string | string[] | undefined};
}) {
// Convert URL search parameters into a format that Coveo's search engine can understand.
const {toSearchParameters} = buildSSRSearchParameterSerializer();
const searchParameters = toSearchParameters(url.searchParams);

// Defines hard-coded context values to simulate user-specific information.
const contextValues = {
ageGroup: '30-45',
mainInterest: 'sports',
};

// Sets the navigator context provider to use the newly created `navigatorContext` before fetching the app static state
const navigatorContext = new NextJsAppRouterNavigatorContext(headers());

setNavigatorContextProvider(() => navigatorContext);

// Fetches the static state of the app with initial state (when applicable)
const staticState = await fetchStaticState({
controllers: {
context: {
Expand All @@ -34,7 +49,12 @@ export default async function Search(url: {
},
},
});
return <SearchPage staticState={staticState}></SearchPage>;
return (
<SearchPage
staticState={staticState}
navigatorContext={navigatorContext.marshal}
></SearchPage>
);
}

// A page with search parameters cannot be statically rendered, since its rendered state should look different based on the current search parameters.
Expand Down
21 changes: 19 additions & 2 deletions packages/samples/headless-ssr/app-router/src/app/react/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@ import ResultList from '@/common/components/react/result-list';
import SearchBox from '@/common/components/react/search-box';
import {SearchPageProvider} from '@/common/components/react/search-page';
import SearchParameterManager from '@/common/components/react/search-parameter-manager';
import {fetchStaticState} from '@/common/lib/react/engine';
import {
fetchStaticState,
setNavigatorContextProvider,
} from '@/common/lib/react/engine';
import {buildSSRSearchParameterSerializer} from '@coveo/headless-react/ssr';
import {headers} from 'next/headers';
import {NextJsAppRouterNavigatorContext} from '../../navigatorContextProvider';

/**
* This file defines a Search component that uses the Coveo Headless library to manage its state.
Expand All @@ -21,13 +26,22 @@ import {buildSSRSearchParameterSerializer} from '@coveo/headless-react/ssr';
export default async function Search(url: {
searchParams: {[key: string]: string | string[] | undefined};
}) {
// Convert URL search parameters into a format that Coveo's search engine can understand.
const {toSearchParameters} = buildSSRSearchParameterSerializer();
const searchParameters = toSearchParameters(url.searchParams);

// Defines hard-coded context values to simulate user-specific information.
const contextValues = {
ageGroup: '30-45',
mainInterest: 'sports',
};

// Sets the navigator context provider to use the newly created `navigatorContext` before fetching the app static state
const navigatorContext = new NextJsAppRouterNavigatorContext(headers());

setNavigatorContextProvider(() => navigatorContext);

// Fetches the static state of the app with initial state (when applicable)
const staticState = await fetchStaticState({
controllers: {
context: {
Expand All @@ -42,7 +56,10 @@ export default async function Search(url: {
});

return (
<SearchPageProvider staticState={staticState}>
<SearchPageProvider
staticState={staticState}
navigatorContext={navigatorContext.marshal}
>
<SearchParameterManager />
<SearchBox />
<ResultList />
Expand Down
43 changes: 43 additions & 0 deletions packages/samples/headless-ssr/app-router/src/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* Middleware for Next.js applications to handle clientID tracking with Coveo.
*
* This middleware generates a unique client ID using the `crypto.randomUUID()` method
* and injects this ID into both the request and response headers under 'x-coveo-client-id'.
* By doing so, it ensures that both server-side and client-side requests can be associated
* with the same client ID, facilitating consistent tracking across requests.
*
* The middleware utilizes Next.js's built-in `NextRequest` and `NextResponse` objects
* from 'next/server' to intercept and modify the requests and responses respectively.
*
* @param {NextRequest} request - The incoming request object provided by Next.js.
* @returns {NextResponse} - The modified response object with the 'x-coveo-client-id' header set.
*/
import {NextRequest, NextResponse} from 'next/server';

export default function middleware(request: NextRequest) {
// Generate the next response object.
const response = NextResponse.next();

// Create a new Headers object based on the incoming request headers.
const requestHeaders = new Headers(request.headers);

// Generate a unique client ID.
const uuid = crypto.randomUUID();

// Assigns the 'x-coveo-client-id' header within the request headers. The specific header name is flexible, provided it remains consistent for client-side retrieval.
requestHeaders.set('x-coveo-client-id', uuid);

// Also set the 'x-coveo-client-id' header in the response headers to ensure
// the client ID is consistent across server-side and client-side requests.
response.headers.set('x-coveo-client-id', uuid);

// Storing document location for NavigatorContextProvider
response.headers.set('x-href', request.nextUrl.href);

// Return the modified response.
return response;
}

export const config = {
matcher: ['/react', '/generic'],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {NavigatorContext} from '@coveo/headless/ssr';
import type {ReadonlyHeaders} from 'next/dist/server/web/spec-extension/adapters/headers';

/**
* Provides navigation context for Coveo within Next.js App Router applications.
* This class is essential for returning the client ID, user agent, and referrer information.
*
* Prior to constructing and hydrating the application's static state, instantiate a navigator context to avoid warnings
*/
export class NextJsAppRouterNavigatorContext implements NavigatorContext {
constructor(private headers: ReadonlyHeaders) {}

get referrer() {
// The referrer is null on first page load (github.com/vercel/next.js/issues/59301)
return this.headers.get('referer') || this.headers.get('referrer'); // Some browsers use 'referer'
}

get userAgent() {
return this.headers.get('user-agent');
}

get location() {
return this.headers.get('x-href');
}

get clientId() {
const clientId = this.headers.get('x-coveo-client-id');
return clientId || crypto.randomUUID();
}

get marshal(): NavigatorContext {
return {
clientId: this.clientId,
location: this.location,
referrer: this.referrer,
userAgent: this.userAgent,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ import {
export const config = {
configuration: {
...getSampleSearchEngineConfiguration(),
analytics: {enabled: false},
analytics: {
analyticsMode: 'next',
trackingId: 'sports-ui-samples',
},
},
controllers: {
context: defineContext(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
'use client';

import {NavigatorContext} from '@coveo/headless/ssr';
import {useEffect, useState} from 'react';
import {useSyncSearchParameterManager} from '../../hooks/generic/search-parameter-manager';
import {
SearchStaticState,
SearchHydratedState,
hydrateStaticState,
setNavigatorContextProvider,
} from '../../lib/generic/engine';
import {HydrationMetadata} from '../common/hydration-metadata';
import {Facet} from './facet';
Expand All @@ -14,13 +16,18 @@ import {SearchBox} from './search-box';

export default function SearchPage({
staticState,
navigatorContext,
}: {
staticState: SearchStaticState;
navigatorContext: NavigatorContext;
}) {
const [hydratedState, setHydratedState] = useState<
SearchHydratedState | undefined
>(undefined);

// Setting the navigator context provider also in client-side before hydrating the application
setNavigatorContextProvider(() => navigatorContext);

useEffect(() => {
const {searchParameterManager, context} = staticState.controllers;
hydrateStaticState({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,34 @@
'use client';

import {NavigatorContext} from '@coveo/headless/ssr';
import {useEffect, useState, PropsWithChildren} from 'react';
import {
SearchStaticState,
SearchHydratedState,
hydrateStaticState,
HydratedStateProvider,
StaticStateProvider,
setNavigatorContextProvider,
} from '../../lib/react/engine';
import {HydrationMetadata} from '../common/hydration-metadata';

interface SearchPageProviderProps {
staticState: SearchStaticState;
navigatorContext: NavigatorContext;
}

export function SearchPageProvider({
staticState,
navigatorContext,
children,
}: PropsWithChildren<SearchPageProviderProps>) {
const [hydratedState, setHydratedState] = useState<
SearchHydratedState | undefined
>(undefined);

// Setting the navigator context provider also in client-side before hydrating the application
setNavigatorContextProvider(() => navigatorContext);

useEffect(() => {
const {searchParameterManager, context} = staticState.controllers;
hydrateStaticState({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,8 @@ const engineDefinition = defineSearchEngine(config);
export type SearchStaticState = InferStaticState<typeof engineDefinition>;
export type SearchHydratedState = InferHydratedState<typeof engineDefinition>;

export const {fetchStaticState, hydrateStaticState} = engineDefinition;
export const {
fetchStaticState,
hydrateStaticState,
setNavigatorContextProvider,
} = engineDefinition;
1 change: 1 addition & 0 deletions packages/samples/headless-ssr/common/lib/react/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const {
hydrateStaticState,
StaticStateProvider,
HydratedStateProvider,
setNavigatorContextProvider,
} = engineDefinition;

export const {
Expand Down
43 changes: 43 additions & 0 deletions packages/samples/headless-ssr/pages-router/src/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* Middleware for Next.js applications to handle clientID tracking with Coveo.
*
* This middleware generates a unique client ID using the `crypto.randomUUID()` method
* and injects this ID into both the request and response headers under 'x-coveo-client-id'.
* By doing so, it ensures that both server-side and client-side requests can be associated
* with the same client ID, facilitating consistent tracking across requests.
*
* The middleware utilizes Next.js's built-in `NextRequest` and `NextResponse` objects
* from 'next/server' to intercept and modify the requests and responses respectively.
*
* @param {NextRequest} request - The incoming request object provided by Next.js.
* @returns {NextResponse} - The modified response object with the 'x-coveo-client-id' header set.
*/
import {NextRequest, NextResponse} from 'next/server';

export default function middleware(request: NextRequest) {
// Generate the next response object.
const response = NextResponse.next();

// Create a new Headers object based on the incoming request headers.
const requestHeaders = new Headers(request.headers);

// Generate a unique client ID.
const uuid = crypto.randomUUID();

// Assigns the 'x-coveo-client-id' header within the request headers. The specific header name is flexible, provided it remains consistent for client-side retrieval.
requestHeaders.set('x-coveo-client-id', uuid);

// Also set the 'x-coveo-client-id' header in the response headers to ensure
// the client ID is consistent across server-side and client-side requests.
response.headers.set('x-coveo-client-id', uuid);

// Storing document location for NavigatorContextProvider
response.headers.set('x-href', request.nextUrl.href);

// Return the modified response.
return response;
}

export const config = {
matcher: ['/react', '/generic'],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {NavigatorContext} from '@coveo/headless/ssr';
import {IncomingHttpHeaders} from 'node:http';

/**
* Provides navigation context for Coveo within Next.js Pages Router applications.
* This class is essential for returning the client ID, user agent, and referrer information.
*
* Prior to constructing and hydrating the application's static state, instantiate a navigator context to avoid warnings
*/
export class NextJsPagesRouterNavigatorContext implements NavigatorContext {
constructor(private headers: IncomingHttpHeaders) {}

get referrer() {
const referrerHeader = this.headers['referer'] ?? this.headers['referrer']; // Some browsers use 'referer'
const referrer = Array.isArray(referrerHeader)
? referrerHeader[0]
: referrerHeader;
return referrer ?? null;
}

get userAgent() {
const userAgentHeader = this.headers['user-agent'];
return Array.isArray(userAgentHeader)
? userAgentHeader[0]
: userAgentHeader;
}

get location() {
const locationHeader = this.headers['x-href'];
const location = Array.isArray(locationHeader)
? locationHeader[0]
: locationHeader;
return location ?? null;
}

get clientId() {
const clientIdHeader = this.headers['x-coveo-client-id'];
const clientId = Array.isArray(clientIdHeader)
? clientIdHeader[0]
: clientIdHeader;
return clientId || crypto.randomUUID();
}

get marshal(): NavigatorContext {
return {
clientId: this.clientId,
location: this.location,
referrer: this.referrer,
userAgent: this.userAgent,
};
}
}
Loading
Loading