Skip to content

Instantly share code, notes, and snippets.

@aweber1
Last active February 27, 2021 02:38
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save aweber1/39d1e9e07fb19aac9bc9b94feb87bb0f to your computer and use it in GitHub Desktop.
Save aweber1/39d1e9e07fb19aac9bc9b94feb87bb0f to your computer and use it in GitHub Desktop.
JSS Rendering Host

Description

The files in this gist demonstrate a fairly basic setup for a JSS rendering host.

DISCLAIMER: No guarantees that the code actually runs as-is. It was largely edited in place, so there may be typos or small syntax errors that you'll need to correct in your editor of choice. Feel free to leave a comment with any necessary fixes.

  • Sitecore config: jss-app-config.config This file is meant to show you how to setup your app config to use an external rendering host.

  • Entry point: jss-rendering-host-tunnel.js This file creates a ngrok tunnel and then starts the rendering host server

  • Rendering host server: jss-rendering-host-server.js This file starts an Express server that is responsible for handling rendering requests from Sitecore JSS as well as serving static assets for your JSS app.

  • App rendering middleware: jss-rendering-host-middleware.js This file implements Express middleware that parses incoming (POST) rendering requests from Sitecore JSS, then renders a JSS app (typically via server.bundle.js) to HTML and returns the HTML as response to the Sitecore JSS request.

IMPORTANT (do not ignore or you will have a bad time)

If you want static assets (e.g. images, css) used by your app to resolve correctly when served by Sitecore, you must ensure that your app and assets are built for rendering remotely and that your rendering host can handle requests for static assets. Your app needs to be built or tokenized specifically for external rendering so that the HTML emitted by SSR uses absolute URLs for static assets (e.g. images, css). The absolute URLs should be the URL of your rendering host or the tunnel that is exposing your rendering host.

While this type of build/token replace process is currenty do-able if your rendering host hostname is known at build time, it becomes more challenging if it's a dynamically-created hostname (e.g. via ngrok) due to the way app bundles are usually built.


For testing/proof-of-concept purposes, if you are OK with your assets not resolving properly (e.g. images, css, js) and are more interested in just proving that the connection between Sitecore and Rendering Host works, then you can do the following:

  • npm run start:rendering-host
    • this will start the rendering host with a ngrok tunnel URL
    • copy the tunnel URL from the console output
    • browse to your Sitecore site with the ?sc_httprenderengineurl=[my ngrok tunnel url] querystring parameter
      • alternatively, if using SXA+JSS, you can edit the JSS site settings item, and set the Server side rendering engine url field to the ngrok tunnel URL. Then save.
    • REMINDER: when browsing in this mode, your app HTML will render, but it's likely that any static assets will 404 because they will be referenced in the HTML with relative URLs, e.g. /static/css/main.css, instead of absolute URLs, e.g. http://renderinghost/static/css/main.css.

Notes about Sitecore config

When the Sitecore JSS rendering engine makes a request to a rendering host, the rendering host URL can be resolved in several ways, here's a general outline of precedence:

  • QueryString: sc_httprenderengineurl parameter takes precedence over all other values when the <AllowOptionsOverride>true</AllowOptionsOverride> setting is true for your JSS app.

  • Session: when the sc_httprenderengineurl querystring parameter is provided, the value will be stored in server session so that you can navigate between routes without always needing to specify the qs param. If you want to clear the session value, end your Sitecore session as you see fit. Or, make a request with ?sc_httprenderengineurl= or ?sc_httprenderengineurl=null.

  • JSS app config: if using SXA, this value is provided by the Server side rendering engine url field in the JSS+SXA site settings. If not using SXA, this value can be set in the <app /> config node via the serverSideRenderingEngineUrl attribute.

  • Rendering engine instance config: each JSS app can have it's own rendering engine configuration, example: App_Config/Sitecore/JavaScriptServices/Sitecore.JavaScriptServices.ViewEngine.Http.Instance.config. Check /sitecore/admin/showconfig.aspx to determine resolved rendering engine config.

  • Rendering engine default config: App_Config/Sitecore/JavaScriptServices/Sitecore.JavaScriptServices.ViewEngine.Http.config

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/" xmlns:role="http://www.sitecore.net/xmlconfig/role/">
<sitecore>
<javaScriptServices>
<apps>
<app name="jss-app-name"
sitecorePath="/sitecore/content/jss-app-name"
useLanguageSpecificLayout="true"
graphQLEndpoint="/api/jss-app-name"
<!-- `serverSideRenderingEngine` is `nodejs` by default, change to `http` to use an external rendering host -->
serverSideRenderingEngine="http"
serverSideRenderingEngineEndpointUrl="http://localhost:5000/jss-render"
inherits="defaults"
/>
</apps>
<renderEngines>
<renderEngine name="http">
<instance id="jss-app-name" inherits="defaults">
<!--
This setting allows you to override the `serverSideRenderingEngineEndpointUrl` value defined above
via a querystring parameter, `?sc_httprenderengineurl=http://custom-host-name:5000/jss-render`
-->
<AllowOptionsOverride>true</AllowOptionsOverride>
</instance>
</renderEngine>
</renderEngines>
</javaScriptServices>
</sitecore>
</configuration>
const { parse } = require('url');
module.exports = {
getJssRenderingHostMiddleware,
};
// `app` is your JSS server bundle or something that can invoke your SSR process
function getJssRenderingHostMiddleware(app) {
return async function middleware(req, res) {
console.log(`Received render request`);
// adjust timeout accordingly or set elsewhere
req.setTimeout(36000, () => {
console.error('request timed out');
});
try {
const parsedUrl = parse(req.url, true);
const jssData = resolveJssData(req);
// render app and return
const renderCallback = getRenderCallback(res);
// By default, jssData.renderFunction will be `renderView`.
// However, as long as you can render your app to HTML you can do whatever you want
// here to get that HTML and return it in the response.
// By default, the request from JSS server expects to receive a response in the format:
// { html: string, statusCode?: int, redirect?: string }
const renderFunction = app[jssData.renderFunctionName];
renderFunction(renderCallback, jssData.renderPath, jssData.layoutData, jssData.viewBag);
} catch (err) {
respondWithError(err, res);
}
};
}
function getRenderCallback(res) {
// NOTE: this renderCallback does not handle non-200 status codes that may be
// returned from your app during rendering. You _may_ want to handle those codes
// here or just continue to pass them back to Sitecore for handling there.
return function renderCallback(errorValue, renderingResult) {
if (errorValue) {
respondWithError(errorValue, res);
} else if (typeof renderingResult !== 'string') {
// successValue is an object/number/etc - JSON-serialize it
let successValueJson = {};
try {
// By default, the request from JSS server expects to receive a response in the format:
// { html: string, statusCode?: int, redirect?: string }
successValueJson = JSON.stringify(renderingResult);
} catch (ex) {
// JSON serialization error - pass it back to http caller.
respondWithError(ex, res);
return;
}
res.setHeader('Content-Type', 'application/json');
res.end(successValueJson);
} else {
// String - can bypass JSON-serialization altogether
res.setHeader('Content-Type', 'text/plain');
res.end(renderingResult);
}
}
}
function respondWithError(errorValue, res) {
console.error(errorValue);
res.statusCode = 500;
res.end(
JSON.stringify({
errorMessage: errorValue.message || errorValue,
errorDetails: errorValue.stack || null,
})
);
}
function resolveJssData(req) {
// We assume req.body has already been parsed as JSON via something like `body-parser` middleware.
const invocationInfo = req.body;
// By default, the JSS server invokes this route with the following body data structure:
// {
// id: 'JSS app name',
// args: ['route path', 'serialized layout data object', 'serialized viewbag object'],
// functionName: 'renderView',
// moduleName: 'server.bundle'
// }
const result = {
layoutData: null,
viewBag: null,
renderPath: '',
renderFunction: null,
};
if (!invocationInfo || !invocationInfo.args || !Array.isArray(invocationInfo.args)) {
return result;
}
result.renderFunctionName = invocationInfo.functionName;
result.renderPath = invocationInfo.args[0];
if (invocationInfo.args.length > 0 && invocationInfo.args[1]) {
result.layoutData = tryParseJson(invocationInfo.args[1]);
}
if (invocationInfo.args.length > 1 && invocationInfo.args[2]) {
result.viewBag = tryParseJson(invocationInfo.args[2]);
}
return result;
}
function tryParseJson(jsonString) {
try {
const json = JSON.parse(jsonString);
// handle non-exception-throwing cases
if (json && typeof json === 'object' && json !== null) {
return json;
}
} catch (e) {
console.error(`error parsing json string '${jsonString}'`, e);
}
return null;
}
const express = require('express');
const bodyParser = require('body-parser');
const { getJssRenderingHostMiddleware } = require('./jss-rendering-host-middleware');
module.exports = {
startRenderingHostServer,
};
function startRenderingHostServer({ port }) {
const resolvedPort = port || process.env.PORT || 5000;
const server = express();
// Handle requests for static assets from the 'static' folder. Change this to suit your app needs.
// NOTE: your app should be built so that all assets are using absolute URLs that point to
// the rendering host server. This way when your app is rendering via Sitecore, asset URLs
// will resolve appropriately.
server.use(express.static('static'));
// Setup the JSS rendering host route. Note that rendering requests from Sitecore JSS are POST requests.
// The URL that is called is configured via JSS app config, e.g. `<app serverSideRenderingEngineEndpointUrl="" />`
server.post('/jss-render', jsonBodyParser, getJssRenderingHostMiddleware(app));
// If using ngrok or other tunneling software, be sure that the rendering host server is listening on the same URL
// that the tunnel is forwarding requests to.
server.listen(resolvedPort, (err) => {
if (err) {
throw err;
}
console.log(`JSS Rendering Host listening on http://localhost:${resolvedPort}`);
});
}
const ngrok = require('ngrok');
const { startRenderingHostServer } = require('./jss-rendering-host-server.js');
const app = require('./server.bundle');
const port = process.env.PORT || 5000;
startRenderHostTunnel('localhost', { port })
.then((tunnelUrl) => {
// Once the tunnel is created, you may want to consider:
// * Using a script to replace URLs in your application bundle with the `tunnelUrl` value.
// * Building your app here.
// * Or use a pre-defined hostname for your tunnel and build your app using that hostname for asset URLs.
startRenderingHostServer({
port,
app
});
})
.catch((err) => {
console.error(err);
});
// This function starts an ngrok tunnel that will, by default,
// expose `localhost:5000` via a public URL, e.g. https://13453.ngrok.io
function startRenderHostTunnel(
renderHostname,
options = { port: 80, proto: 'http', quiet: false }
) {
if (!renderHostname) {
throw new Error(
'Unable to start render host tunnel as no hostname for the rendering host was specified.'
);
}
const rewriteHost = `${renderHostname}:${options.port}`;
const finalOptions = {
...options,
host_header: 'rewrite',
addr: rewriteHost,
}
return ngrok
.connect(finalOptions)
.then((url) => {
if (!options.quiet) {
console.log(`Tunnel started, forwarding '${url}' to '${rewriteHost}'`);
}
return url;
});
}
{
"scripts": {
"start:rendering-host": "node ./jss-rendering-host-tunnel.js"
},
"dependencies": {
"body-parser": "^1.19.0",
"express": "^4.17.1",
"ngrok": "^3.2.5"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment