Server-Side Rendering (SSR) in Meteor
Meteor platform is still alive! Of course, it is, and it is constantly improved. It isn’t as dynamic a process as in the past, but I hope Meteor will someday be a complete and flexible platform. Maybe it needed to slow down a little bit. For me, the most exciting improvement lately is Server Side Rendering. I’ll try to tell you why, and especially why I like it in Meteor.
What we will cover here:
- Why is Server-Side Rendering (SSR) good to have, especially in Meteor?
- How is it implemented in Meteor?
- Real example in ready-to-use boilerplate.
Why is Server-Side Rendering (SSR) good to have, especially in Meteor?
Some time ago, I wrote an article about why I keep coming back to Meteor. I wrote that I use Meteor in projects that aren’t that demanding because of some of its limitations. Anyways, it is still a complete platform, and it’s straightforward to start. Of course, I still think this is the best platform for fast prototyping and not-so-complicated projects, but if it improves, I am sure it will also be suitable for big apps (just my opinion). It has ultra simple deployment options, and the build system works automatically. This is why there are so many devs who like it. And now we have SSR on board with a couple of other excellent features like dynamic imports (topic for a separate article).
Server-Side Rendering is probably the most sought feature in website-like projects. So, projects which aren’t very big. Projects which are half apps and half websites. Why is it nice? Because you can prepare a website that is accessible for all web crawlers out there. Your SEO will shine with that. Of course, Google bots should index your website anyway, but what with Facebook or Twitter bots? What with sharing materials on social services? I always had problems with that, even when building simple projects.
Having SSR on board with simple configuration is what I like in all frameworks. Of course, there will be many situations when you will choose not to use SSR because you have much more benefits from not using it. For example, separated front-end and back-end (different servers), or maybe you don’t need it in the app because you want to build an internally used app. Or all content and functionalities are just available for private access, only for logged-in users, so you don’t care. There are always drawbacks, and you need to decide.
You’ll probably think, so how did it all work without that before? There are a couple of options. SSR isn’t something that is a significant improvement. You can implement some workarounds, which are also very good. You can use services like Prerender.io. You can use your server, which will generate static websites and serve them for web crawlers only. I guess this is just a matter of choosing what is most valuable in the app you’re building.
So, why do I like SSR, especially in Meteor? Because this is the next missing puzzle in a very simple to start platform. Without any significant configuration and other preparations. You can focus on coding. So, it is much better for me to use it with Meteor because I can get not only SSR but the whole stack up and running very fast. Of course, there are plenty of options out there, other frameworks, etc., but it is always better to have such features in a platform you know.
How it is implemented in Meteor?
The most exciting part of this article, so let’s see what it looks like without further ado.
First of all, you need a new package called server-render, which needs to be added to your Meteor app. You can do this by writing in the terminal: meteor add server-render
. Unlike dynamic-import package, this one isn’t added when creating a new Meteor app. I think this is good. You’ll be able to decide and add it if only needed.
After you add the package, you need to be sure that your components will be accessible on both the server and client-side. I use React, so this is very simple to achieve. I am not sure how it looks with, for example, Angular. Later you’ll see how I’ve got it structured in my simple boilerplate. I haven’t tested it yet, but the package should work with all modern front-end frameworks integrated with Meteor. For now, let’s see what the API looks like.
So the server-render
package exposes the onPageLoad
method and sink
object with proper methods, allowing you to modify the HTML that the server will serve. On the server side, we need to parse our main App component to the string using built-in React methods (so, the standard way of doing SSR with React). Then we can use sink.renderIntoElementById
to put the full HTML in a proper place in the body. onPageLoad
will fire on every request, so the current source code of the page should be updated. You can use other methods from sink
. You can append some elements to the head tag. For example, you can use react-helmet on the server, which you should do. I strongly recommend you that. This was always problematic, especially when you want to share your content on social services like Twitter or Facebook. For example, you can inject Open Graph tags and other meta tags so Facebook or Twitter bots will see them correctly. This is very useful.
I don’t want to copy and paste the complete documentation here, and you’ll find more in the Readme file of the package linked above. Instead of just documentation, I want to jump to the actual example: my simple boilerplate. That is the old boilerplate that was recently updated, so there is probably some mess in it. But this is not the case for now. Let’s see how SSR looks like there.
Real example in ready-to-use boilerplate.
Lately, I’ve updated my Meteor boilerplate to be ready for SSR. It also uses Redux, so it will be an excellent opportunity to show how to use Redux and SSR at once.
This is a pretty standard boilerplate based on Meteor Guide, so the folders structure should be simple for every Meteor developer. I have client app initialization and server-side app initialization. I also share React routes and Redux actions and reducers between the server and client.
Let’s see what we have on the client side first.
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Switch } from 'react-router-dom';
import { onPageLoad } from 'meteor/server-render';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
import { createStore, applyMiddleware } from 'redux';
import routes from '../both/routes';
import mainReducer from '../../api/redux/reducers';
const preloadedState = window.__PRELOADED_STATE__; // eslint-disable-line
delete window.__PRELOADED_STATE__; // eslint-disable-line
const store = createStore(mainReducer, preloadedState, applyMiddleware(thunk));
const App = () => (
<Provider store={store}>
<BrowserRouter>
<Switch>
{routes}
</Switch>
</BrowserRouter>
</Provider>
);
onPageLoad(() => {
ReactDOM.render(<App />, document.getElementById('app'));
});
As you can see, I use Redux, so when using SSR, I can get the initial state for the store on the server and pass it in the static generated HTML. This is recommended way. Here on the client-side, I get the initial state from a special object injected into the body. You’ll see how it’s done later. Then I create the Redux store with initial data and prepare the standard main App component with routing. I use React Router 4 here. This is all that you need on the client-side for now.
Let’s see what we have on the server side.
import React from 'react';
import { renderToString } from 'react-dom/server';
import { onPageLoad } from 'meteor/server-render';
import { StaticRouter } from 'react-router';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import { object } from 'prop-types';
import { Helmet } from 'react-helmet';
import mainReducer from '../../api/redux/reducers';
import routes from '../both/routes';
import { todosGetAll } from '../../api/todos/methods';
onPageLoad((sink) => {
const context = {};
const initial = todosGetAll.call({});
const store = createStore(mainReducer, { todos: initial }, applyMiddleware(thunk));
const App = props => (
<Provider store={store}>
<StaticRouter location={props.location} context={context}>
{routes}
</StaticRouter>
</Provider>
);
App.propTypes = {
location: object.isRequired,
};
const preloadedState = store.getState();
sink.renderIntoElementById('app', renderToString(<App location={sink.request.url} />));
const helmet = Helmet.renderStatic();
sink.appendToHead(helmet.meta.toString());
sink.appendToHead(helmet.title.toString());
sink.appendToBody(`
<script>
window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState).replace(/</g, '\\u003c')}
</script>
`);
});
Here is where all magic happens. We need to wrap all the code in the onPageLoad
method to rerun it and generate new static HTML on every page request. Configuration is similar. Here we can create the initial state for our Redux store. Then we can create the store and prepare the main
component. What is different here? We use static router configuration here because, on the server, there isn’t browser history, etc. Another thing that needs to be done is putting the initial state into our static HTML code. We do this using sink.appendToBody
method from server-render
package. All main code is generated using React’s renderToString
, and then it is injected into the app div element using sink.renderIntoElementById
.
What else can we do here? Of course, we can inject some meta tags, like Open Graph tags or just title and description for every page. We can do this using the react-helmet
library. We use the Helmet.renderStatic
method, which returns data that then can be appended to the tag of our static document. Of course, this will be different data for every page. We use the sink.appendToHead
method to be able to do that.
That’s it. Having this API, we can straightforwardly configure SSR. This is another part of Meteor which makes this platform very pleasant to use for every developer. Let me know what you think. Let me know how you use it in your projects.
To sum up this short article.
I just wanted to research the newest SSR options in Meteor, and I must say that I like what I saw. I think Meteor is going in the right direction. It is faster and uses more and more of the newest approaches to the build processes. Apollo Integration also looks good. The only missing things for me are a more configurable and flexible build system and a more extendable accounts system decoupled from the Meteor and Live Data. Or at least more documentation on that. All other stuff looks excellent.