Skip to content

Instantly share code, notes, and snippets.

@jfhector
Created February 24, 2022 14:23
Show Gist options
  • Save jfhector/ba24dfbf4f2ddf76be78c362d208f674 to your computer and use it in GitHub Desktop.
Save jfhector/ba24dfbf4f2ddf76be78c362d208f674 to your computer and use it in GitHub Desktop.
Getting a value from a request header, and using that value to show a banner – both Server-Side and Client-Side rendered

Getting a value from a request header, and using that value to show a banner – both Server-Side and Client-Side rendered

How this works

### In the renderApp Express route handler

  1. We look at the value of a request header with the visitor's location country.
  2. We pass that value to <App visitorLocationCountry={visitorLocationCountry} /> (i.e. App being called when rendered to string on the server).
  3. We generate a HTML meta tag string using that visitor code, and put that in the head of the generated index.html document

In the client-side index.tsx (where App is called client side)

  1. We query the document to find that meta tag and get visitorLocationCountry
  2. We call app in the same way as on the server: <App visitorLocationCountry={visitorLocationCountry} />

Using the visitorLocationCountry in the PageWrapper's component (which App renders)

  1. App passes the visitorLocationCountry prop to PageWrapper.

    • Note: it's hard to use Context, because the Context object needs to be created inside the App function definition (because it needs the visitorLocationCountry prop). That means that that Context object can't easily be exported, for other components to import.
  2. PageWrapper uses the value inside visitorLocationCountry to decide whether or not to render the banner

/* istanbul ignore file */
import 'ts-polyfill/lib/es2015-core';
import 'ts-polyfill/lib/es2015-promise';
import 'ts-polyfill/lib/es2016-array-include';
import 'whatwg-fetch';
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router } from 'react-router-dom';
import { initFathom } from './utils/analyticsEvents';
import { App } from './App';
import { Entry } from 'contentful';
import { PageContainingNavigationBarAndFooter } from './components/PageWrapper';
declare global {
interface Window {
config: Config;
__initial_content?: Entry<PageContainingNavigationBarAndFooter>;
__initial_content_url?: string;
}
}
initFathom();
const visitorLocationCountryMetaTag = document.querySelector(
'meta[name="visitor-location-country"][content]'
) as HTMLMetaElement | null; // TODO: Replace by an assertion function or similar
const visitorLocationCountry = visitorLocationCountryMetaTag?.content;
ReactDOM.render(
<Router>
<App visitorLocationCountry={visitorLocationCountry} />
</Router>,
document.getElementById('root')
);
/* eslint-disable no-console */
// TODO Update docs (see previous version)
function generateVisitorLocationCountryMetaTag(
visitorLocationCountry: string | null | undefined
): string | null {
if (!visitorLocationCountry) return null;
return `<meta name="visitor-location-country" content="${visitorLocationCountry}">`;
}
export { generateVisitorLocationCountryMetaTag };
/* eslint-disable no-console */
import { Request } from 'express';
const HEADER_WITH_VISITORS_LOCATION_COUNTRY = 'CF-IPCountry';
type GetVisitorLocationCountry = {
(req: Request): string | null;
};
// TODO Add docs (see previous version)
const getVisitorLocationCountry: GetVisitorLocationCountry = req => {
const visitorLocationCountryFromHeader = req.get(
HEADER_WITH_VISITORS_LOCATION_COUNTRY
);
if (!visitorLocationCountryFromHeader) return null;
return visitorLocationCountryFromHeader.toLowerCase();
};
export { getVisitorLocationCountry };
import { Response, Request } from 'express';
import React from 'react';
import ReactDOM from 'react-dom/server';
import { StaticRouter } from 'react-router-dom';
import { ServerStyleSheet } from 'styled-components';
import { App } from '../../client/App';
import favicon from '../../client/images/favicon.svg';
import configProvider from '../../client/utils/configProvider';
import getContent from '../../client/utils/getContent';
import { getContentUrl } from '../../client/utils/getContentUrl';
import { PageContainingNavigationBarAndFooter } from '../../client/components/PageWrapper';
import generateHtml from '../generateHtml';
import { Entry } from 'contentful';
import {
getVisitorLocationCountry,
generateVisitorLocationCountryMetaTag,
} from './visitorLocationCountry';
const sheet = new ServerStyleSheet();
export default async (req: Request, res: Response): Promise<void> => {
const config = configProvider.config;
const contentToFetch = config.pages[req.path as keyof Pages];
const slug = req.params.slug;
let contentUrl;
if (slug) {
contentUrl = getContentUrl('article', slug, contentToFetch, req.hostname);
} else if (contentToFetch) {
contentUrl = getContentUrl(undefined, slug, contentToFetch, req.hostname);
}
let content;
if (contentUrl) {
try {
content = await getContent<Entry<PageContainingNavigationBarAndFooter>>(
contentUrl
);
} catch (err) {
console.log(err);
}
}
const visitorLocationCountry = getVisitorLocationCountry(req);
// TODO Document the business with visitorLocationCountry
const AppWithRouter = (
<StaticRouter location={req.path}>
<App
serverContent={content}
visitorLocationCountry={visitorLocationCountry}
/>
</StaticRouter>
);
const bodyHtml = ReactDOM.renderToString(sheet.collectStyles(AppWithRouter));
const styles = sheet.getStyleTags();
const additionalContentForHead =
generateVisitorLocationCountryMetaTag(visitorLocationCountry) ?? '';
const html = generateHtml(
bodyHtml,
content,
styles,
favicon,
req.originalUrl,
contentUrl || '',
additionalContentForHead
);
res.send(html);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment