Hydrate Piral from Server-Side Rendering
⚡ A sample Node.js server for server-side rendering (SSR) a Piral instance.
For running this sample locally all you need to do after cloning this repository is running:
npm i && npm run build && npm start
Remark: This sample requires Node.js and NPM. The used port is 3000
, which could be re-configured easily (e.g., via an environment variable PORT
).
There are multiple levels of SSR. Every level adds another piece of complexity. The jumps are getting higher per level.
- CSR, leave everything as-is
- "shallow" ("level-0") SSR that places the feed service response in the delivered code
- "level-1" SSR that places the (JS-bundles from the) pilets from the feed service response in the delivered code
- "level-2" SSR that pre-evaluates the setup methods of the pilets; the initial state is already delivered and the pilets are all bundled together with the main code (setup will not be run again)
- "level-3" SSR that does not stop at the setup method - actually the whole thing is rendered and hydrated from there again
More details on these levels are in the original issue at GitHub.
Right now we are at level-1 SSR, i.e., we still require the client to do the heavy lifting, but we can boost the startup time drastically by embedding all necessary (dynamic) resources (i.e., the pilets) in a single response.
Remark: Most likely we will never reach level-3 as this is way too complex; not only for Piral itself, but also for developers using Piral. In this case pilets would need to be developed super careful such that they just work without a real DOM underneath. This kind of development is unlikely.
The application is essentially split in two parts:
- A client-side entry point via
src/client/index.tsx
(effectively used bysrc/client/index.html
) - A server-side entry point via
src/server/index.tsx
usingpiral-ssr-utils
Both parts are using src/common/app.tsx
for accessing the application. The only difference between the client and server is that the latter is just taking care about retrieving a string
, while the former performs an hydration on the DOM.
The state is already predetermined (see src/common/instance.tsx
) and will be evaluated equally on the server and the client. This results in the right rendering from the server and the client.
There are two crucial parts. The first one is how to retrieve the pilets later on:
import * as React from 'react';
import { createInstance } from 'piral-core';
import { configForServerRendering } from 'piral-ssr-utils/runtime';
export function createAppInstance() {
return createInstance(
configForServerRendering({
// ... standard configuration
}),
);
}
By using configForServerRendering
the pilets are picked up from the window
if available, otherwise we just an empty array of pilets. In the latter case we assume we are in debug mode.
But how are the pilets entering the window
global? For this we need to look what the SSR actually returns to the browser:
import { renderFromServer } from 'piral-ssr-utils';
async function sendIndex(_: express.Request, res: express.Response) {
const app = createApp();
const content = await renderFromServer(app, {
getPilet(url) {
return readRemoteText(url);
},
async getPiletsMetadata() {
const res = await readRemoteJson(feedUrl);
return res.items;
},
fillTemplate(body, script) {
return indexHtml
.replace('<div id="app"></div>', `<div id="app">${body}</div>`)
.replace('<noscript id="data"></noscript>', script);
},
});
res.send(content);
}
We set the window.__pilets__
with another script (called script
), which is run just before including our application. The renderFromServer
helper is actually just transforming the feed response (including remote links) to a plain response including the content in form content
.
Piral and this sample code is released using the MIT license. For more information see the license file.