jss icon indicating copy to clipboard operation
jss copied to clipboard

Provide a way to pass UTM parameters to RestLayoutService.fetchPlaceholderData

Open thaiphan opened this issue 2 years ago • 5 comments

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?

thaiphan avatar Jun 30 '22 02:06 thaiphan

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();

ChrisManganaro avatar Jul 07 '22 06:07 ChrisManganaro

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.

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

pzi avatar Jul 07 '22 09:07 pzi

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);

pzi avatar Jul 07 '22 11:07 pzi

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?

pzi avatar Jul 08 '22 07:07 pzi

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

ambrauer avatar Jul 25 '22 19:07 ambrauer