for reference, here is a (somewhat messy) ssr code snipped we use (unaltered and undocummented). It also uses a page cache in mongodb:
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import { FastRender } from 'meteor/staringatlights:fast-render';
import { InjectData } from 'meteor/staringatlights:inject-data';
import { Random } from 'meteor/random';
import { Meteor } from 'meteor/meteor';
import moment from 'moment';
import { renderToString } from 'react-dom/server';
import React from 'react';
import Radium from 'radium';
import { Helmet } from 'react-helmet';
import Loadable from 'react-loadable';
import { PageNotFoundError } from '/imports/api/errors';
import { PageCache } from '/imports/api/collections';
import app from '/imports/modules/app';
InjectData.disableInjection = true;
const RadiumWrapper = Radium(
class extends React.Component {
/* eslint react/destructuring-assignment: 0 */
render() {
return this.props.children;
}
}
);
app.init();
const send404 = sink => {
sink.setStatusCode(404);
};
const renderTheRoute = ({ sink, Component, props, matchinRoute }) => {
// we need to keep the random seed accress cached pages
// as the random seed has only visual implications, that's not a big problem
// result is that every page with the same random seed looks the same
const randomSeed = Random.id();
const modules = [];
const modulesResolved = [];
const { url, query: queryParams } = sink.request;
const { pathname: path } = url;
const { route, params } = matchinRoute;
FlowRouter.setCurrent({
path,
params,
route,
queryParams,
});
const appString = renderToString(
<Loadable.Capture
report={moduleName => {
modules.push(moduleName);
}}
reportResolved={resolvedModuleName => {
modulesResolved.push(resolvedModuleName);
}}
>
<RadiumWrapper radiumConfig={{ userAgent: 'all' }}>
<Component {...props} randomSeed={randomSeed} />
</RadiumWrapper>
</Loadable.Capture>
);
const helmet = Helmet.renderStatic();
const headParts = [
helmet.title.toString(),
helmet.meta.toString(),
helmet.link.toString(),
];
// see https://github.com/abecks/meteor-fast-render/issues/23
FastRender._mergeFrData(sink.request, FastRender.frContext.get().getData());
const fastRenderData = sink.request.headers._injectPayload;
const bodyParts = [
`<script> var __randomSeed = "${randomSeed}";</script>`,
`<script> var __preloadables__ = ${JSON.stringify(
modulesResolved
)};</script>`,
`<script type="text/inject-data">${InjectData.encode(
fastRenderData
)}</script>`,
];
return { bodyParts, headParts, appString };
};
const getCacheKey = sink => `${sink.request.url.path}`;
const sendMatchingRoute = ({ matchinRoute, sink, path, queryParams }) => {
const requestId = Random.id();
const { route, params } = matchinRoute;
if (route.options?.triggersEnter?.length > 0) {
// this is not totally correct as we should respect triggers stop and redirect
// we use it only for i18n route, so for us its fine i guess
route.options.triggersEnter.forEach(trigger =>
trigger({ ...route, params }, () => null, () => null)
);
}
route.render = function(Component, props) {
// currently disabled
const useCache = !process.env.CACHE_DISABLED && !Meteor.userId();
if (useCache) {
const cacheKey = getCacheKey(sink);
if (
!PageCache.findOne({
_id: cacheKey,
updatedAt: {
$gte: moment()
.subtract(5, 'minutes')
.toDate(),
},
})
) {
const doc = {
_id: cacheKey,
updatedAt: new Date(),
...renderTheRoute({
Component,
props,
sink,
matchinRoute,
}),
};
console.log('cache update');
PageCache.upsert(cacheKey, { $set: doc });
} else {
console.log('cache hit');
}
const { headParts, bodyParts, appString } = PageCache.findOne(cacheKey);
headParts.forEach(part => sink.appendToHead(part));
bodyParts.forEach(part => sink.appendToBody(part));
sink.renderIntoElementById('react-root', appString);
} else {
console.log('no cache will be used (logged in)');
const { headParts, bodyParts, appString } = renderTheRoute({
Component,
props,
sink,
matchinRoute,
});
headParts.forEach(part => sink.appendToHead(part));
bodyParts.forEach(part => sink.appendToBody(part));
sink.renderIntoElementById('react-root', appString);
}
};
const data = route.data ? route.data(params, queryParams) : null;
console.log('================');
console.log(`render-${requestId}, ${path}`);
console.time(`rendertime-${requestId}--${path}`);
try {
route.action(params, queryParams, data, { sink });
} catch (e) {
if (e instanceof PageNotFoundError) {
send404(sink);
} else {
console.error('server render failed', e);
}
}
console.timeEnd(`rendertime-${requestId}--${path}`);
console.log('================');
};
Loadable.preloadAll().then(() => {
FastRender.onPageLoad(async sink => {
const { url, query: queryParams } = sink.request;
const { pathname: path } = url;
if (path && path.startsWith('/__')) {
// skip internal endpoints like __cordova
// see https://github.com/meteor/meteor/issues/10557
return;
}
const matchinRoute = FlowRouter.matchPath(path);
if (matchinRoute) {
sendMatchingRoute({
matchinRoute,
sink,
path,
queryParams,
});
} else {
send404(sink);
}
});
});