jss
jss copied to clipboard
Provide a way to pass UTM parameters to RestLayoutService.fetchPlaceholderData
Description
We're currently experimenting SSG and client-side personalisation using RestLayoutService.fetchPlaceholderData
(very similar to the strategy described at https://www.adamlamarre.com/using-fetchplaceholderdata-for-personalization-on-static-sitecore-jss-sites/):
const PersonalisedPlaceholder = () => {
const {asPath} = useRouter();
React.useEffect(() => {
const layoutService = layoutServiceFactory.create();
// Fetch and consume the personalised placeholder data
layoutService.fetchPlaceholderData(name, asPath).then((placeholderData) => {
console.log(placeholderData)
})
}, [])
}
With our current code, the following URL path works...
// http://localhost:3000/why-we-are-fast
layoutService.fetchPlaceholderData('content', '/why-we-are-fast')
However, the following code breaks....
// http://localhost:3000/why-we-are-fast?utm_medium=social
layoutService.fetchPlaceholderData('content', '/why-we-are-fast?utm_medium=social')
Possible Fix
Can you provide a field to RestLayoutService.fetchPlaceholderData
so that we can pass query params to the placeholder service?
Or maybe you guys can take /why-we-are-fast?utm_medium=social
and properly set up the query params for us as part of the SDK?
I have come up with a workaround for the time being, but as mentioned by @thaiphan it would be nice to be able to pass query params directly into fetchPlaceholderData
or fetchLayoutData
methods.
// We are duplicating a bunch of code from the jss repo so we can override the fetcher method.
// All is a copy-paste, except for the returned fetcher method called out below
// @see: https://github.com/Sitecore/jss/blob/4e85a70c521f76535b3b36f73bcc9f69874d2408/packages/sitecore-jss/src/layout/rest-layout-service.ts
const setupReqHeaders = (req: IncomingMessage) => {
return (reqConfig: AxiosRequestConfig) => {
debug.layout('performing request header passing');
reqConfig.headers.common = {
...reqConfig.headers.common,
...(req.headers.cookie && {cookie: req.headers.cookie}),
...(req.headers.referer && {referer: req.headers.referer}),
...(req.headers['user-agent'] && {'user-agent': req.headers['user-agent']}),
...(req.connection.remoteAddress && {'X-Forwarded-For': req.connection.remoteAddress}),
};
return reqConfig;
};
};
const setupResHeaders = (res: ServerResponse) => {
return (serverRes: AxiosResponse) => {
debug.layout('performing response header passing');
serverRes.headers['set-cookie'] && res.setHeader('set-cookie', serverRes.headers['set-cookie']);
return serverRes;
};
};
const dataFetcherResolver: DataFetcherResolver = <T>(req?: IncomingMessage, res?: ServerResponse) => {
const config = {
debugger: debug.layout,
} as AxiosDataFetcherConfig;
if (req && res) {
config.onReq = setupReqHeaders(req);
config.onRes = setupResHeaders(res);
}
const axiosFetcher = new AxiosDataFetcher(config);
// Here is our custom implementation of the fetcher function
return (url: string, data?: unknown) => {
if (req?.url && !req.url.includes('_next')) {
const reqUrl = new URL(req.url, publicUrl);
// if we have get params on our request url
// forward them onto the call to the layout service
if (reqUrl.search) {
const fetchUrl = new URL(url);
const combinedUrlParams = new URLSearchParams({
...Object.fromEntries(fetchUrl.searchParams),
...Object.fromEntries(reqUrl.searchParams),
});
url = `${fetchUrl.origin}${fetchUrl.pathname}?${combinedUrlParams.toString()}`;
}
}
return axiosFetcher.fetch<T>(url, data);
};
};
export class LayoutServiceFactory {
create(additionalConfig?: Partial<RestLayoutServiceConfig>) {
return new RestLayoutService({
apiHost: sitecoreConfig.sitecoreApiHost,
apiKey: sitecoreConfig.nextPublicSitecoreApiKey,
siteName: sitecoreConfig.jssAppName,
configurationName: 'jss',
tracking: false,
dataFetcherResolver,
...additionalConfig,
});
}
}
export const layoutServiceFactory = new LayoutServiceFactory();
I have come up with a workaround for the time being, but as mentioned by @thaiphan it would be nice to be able to pass query params directly into
fetchPlaceholderData
orfetchLayoutData
methods.... return (url: string, data?: unknown) => { if (req?.url && !req.url.includes('_next')) { ... }; }; ...
Note: this only works for getServerSideProps
as the req
is undefined
when using getStaticProps
as the next context
does not contain req
/res
params 😢
This seems to work on the client:
export const processQueryParam = (url: string): string => {
const completeURL = new URL(url);
const item = completeURL.searchParams.get('item') ?? '';
const itemURL = new URL(item, publicUrl);
if (!itemURL.search) {
return url;
}
const itemPath = itemURL.pathname;
const itemQueryParams = itemURL.searchParams;
// append all query params that were on the item to the rest of the url
itemQueryParams.forEach((value, name) => {
completeURL.searchParams.append(name, value);
});
// update the existing item with the path (without query params)
completeURL.searchParams.set('item', itemPath);
return completeURL.href;
};
url = processQueryParam(url)l
return axiosFetcher.fetch<T>(url, data);
I have spent some more time and rewritten the above solution. I've put the complete layout-service-factory.ts
file into the following gist alongside some tests to show the behaviour:
https://gist.github.com/pzi/2051a5eff32bcc458c67a92a86d7b030
@ambrauer @sc-addypathania could we get some feedback on the OG issue and potential solution please?
@thaiphan @ChrisManganaro @pzi Thanks for the feedback, I've added this to our backlog to investigate / fix in a future release.
Just taking a quick look through the details, I'd imagine from an API perspective we'd lean towards adding something like an "addlParams" object parameter to the fetch methods which would be merged with the other query string params and then passed on to the fetchData
method.
We also gladly accept pull requests if you'd like to take a stab at it. Check the contributing guide and we'll be happy to merge it in.
Btw, with the workarounds you posted, you could avoid some of the code duplication by creating your own class that extends the RestLayoutService
. You'd then have access to the setupReqHeaders
and setupResHeaders
for example.