Skip to content

Instantly share code, notes, and snippets.

@kamsar

kamsar/server.js Secret

Created August 1, 2018 18:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kamsar/29370b58f7a4fb15bed74521e01ad6bc to your computer and use it in GitHub Desktop.
Save kamsar/29370b58f7a4fb15bed74521e01ad6bc to your computer and use it in GitHub Desktop.
import serializeJavascript from 'serialize-javascript';
import React from 'react';
import { StaticRouter, matchPath } from 'react-router-dom';
import { renderToStringWithData } from 'react-apollo';
// [CS] ADDED FOR CODE SPLITTING
import Loadable from 'react-loadable';
import Helmet from 'react-helmet';
import GraphQLClientFactory from '../src/lib/GraphQLClientFactory';
import config from '../src/temp/config';
import i18ninit from '../src/i18n';
import AppRoot, { routePatterns } from '../src/AppRoot';
import { setServerSideRenderingState } from '../src/RouteHandler';
import indexTemplate from '../build/index.html';
// [CS] ADDED FOR CODE SPLITTING
import manifest from '../build/asset-manifest.json';
/**
* [CS] Converts a CRA asset manifest into script tags, given a list of used modules
* produced by Loadable.Capture.
*
* This function is used instead of react-loadable's getBundles() function because
* CRA's webpack is not configurable to use the loadable plugin,
* so we consume CRA's slightly different format chunk manifest instead.
*
* We need these script tags to avoid a flash of "loading..." while the browser hydrates the
* code-split components. By telling the browser in advance which components we need,
* we can have their modules preloaded before the page renders.
*/
function convertLoadableModulesToScripts(usedModules) {
return Object.keys(manifest)
.filter((chunkName) => usedModules.indexOf(chunkName.replace('.js', '')) > -1)
.map((k) => `<script src="${manifest[k]}"></script>`)
.join('');
}
/**
* Main entry point to the application when run via Server-Side Rendering,
* either in Integrated Mode, or with a Node proxy host like the node-headless-ssr-proxy sample.
* This function will be invoked by the server to return the rendered HTML.
* @param {Function} callback Function to call when rendering is complete. Signature callback(error, successData).
* @param {string} path Current route path being rendered
* @param {string} data JSON Layout service data for the rendering from Sitecore
* @param {string} viewBag JSON view bag data from Sitecore (extensible context stuff)
*/
export function renderView(callback, path, data, viewBag) {
try {
const state = parseServerData(data, viewBag);
setServerSideRenderingState(state);
/*
GraphQL Data
The Apollo Client needs to be initialized to make GraphQL available to the JSS app.
Not using GraphQL? Remove this, and the ApolloContext from `AppRoot`.
*/
const graphQLClient = GraphQLClientFactory(config.graphQLEndpoint, true);
// [CS] Stores the list of loadable modules that were rendered during SSR
// we use this list to add <script> tags to import all of them to the SSR'ed HTML
const loadableModules = [];
/*
App Rendering
*/
initializei18n(state)
// [CS] make react-loadable preload all dynamic imports - this will get us HTML during SSR
.then(() => Loadable.preloadAll())
.then(() =>
// renderToStringWithData() allows any GraphQL queries to complete their async call
// before the SSR result is returned, so that the resulting HTML from GQL query results
// is included in the SSR'ed markup instead of whatever the 'loading' state is.
// Not using GraphQL? Use ReactDOMServer.renderToString() instead.
renderToStringWithData(
// [CS] loadable.capture makes a list of code-split components that were used during SSR,
// so that we can pre-load them as client-side scripts to avoid any flashes of 'loading...' text.
<Loadable.Capture report={(module) => loadableModules.push(module)}>
<AppRoot path={path} Router={StaticRouter} graphQLClient={graphQLClient} />
</Loadable.Capture>
)
)
.then((renderedAppHtml) => {
const helmet = Helmet.renderStatic();
// We remove the viewBag from the server-side state before sending it back to the client.
// This saves bandwidth, because by default the viewBag contains the translation dictionary,
// which is better cached as a separate client HTTP request than on every page, and HTTP context
// information that is not meaningful to the client-side rendering.
// If you wish to place items in the viewbag that are needed by client-side rendering, this
// can be removed - but still delete state.viewBag.dictionary, at least.
delete state.viewBag;
// We add the GraphQL state to the SSR state so that we can avoid refetching queries after client load
// Not using GraphQL? Get rid of this.
state.APOLLO_STATE = graphQLClient.cache.extract();
// Inject the rendered app into the index.html template (built from /public/index.html)
// IMPORTANT: use serialize-javascript or similar instead of JSON.stringify() to emit initial state,
// or else you're vulnerable to XSS.
const html = indexTemplate
// write the React app
.replace('<div id="root"></div>', `<div id="root">${renderedAppHtml}</div>`)
// write the string version of our state
.replace('__JSS_STATE__=null', `__JSS_STATE__=${serializeJavascript(state)}`)
// render <head> contents from react-helmet
.replace(
'<head>',
`<head>${helmet.title.toString()}${helmet.meta.toString()}${helmet.link.toString()}`
)
// [CS] We inject our used code-split modules' script tags before any other script tags.
// In this case, `<script>` is the opening tag to the __JSS_STATE__ script block in index.html.
.replace('<script>', `${convertLoadableModulesToScripts(loadableModules)}<script>`);
callback(null, { html });
})
.catch((error) => callback(error, null));
} catch (err) {
// need to ensure the callback is always invoked no matter what
// or else SSR will hang
callback(err, null);
}
}
/**
* Parses an incoming url to match against the route table. This function is implicitly used
* by node-headless-ssr-proxy when rendering the site in headless mode. It enables rewriting the incoming path,
* say '/en-US/hello', to the path and language to pass to Layout Service (a Sitecore item path), say
* { sitecoreRoute: '/hello', lang: 'en-US' }.
* This function is _not_ used in integrated mode, as Sitecore's built in route parsing is used.
* If no URL transformations are required (i.e. single language site), then this function can be removed.
* @param {string} url The incoming URL to the proxy server
* @returns { sitecoreRoute?: string, lang?: string }
*/
export function parseRouteUrl(url) {
if (!url) {
return null;
}
let result = null;
// use react-router-dom to find the route matching the incoming URL
// then return its match params
routePatterns.forEach((pattern) => {
const match = matchPath(url, { pattern });
if (match && match.params) {
result = match.params;
}
});
return result;
}
function parseServerData(data, viewBag) {
const parsedData = data instanceof Object ? data : JSON.parse(data);
const parsedViewBag = viewBag instanceof Object ? viewBag : JSON.parse(viewBag);
return {
viewBag: parsedViewBag,
sitecore: parsedData && parsedData.sitecore,
};
}
function initializei18n(state) {
// don't init i18n for not found routes
if (!state || !state.sitecore || !state.sitecore.context) return Promise.resolve();
return i18ninit(state.sitecore.context.language, state.viewBag.dictionary);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment