Pup

v1.x

The BasicsSSR (Server Side Rendering)

Traditionally, applications based on the Meteor JavaScript platform only support client-side rendering. This means that when a user visits our application, instead of pages being built on the server first, the templates that comprise those pages are sent to the client and then the pages are built on the client and displayed to the user.

Contrasting this with server-side rendering, when a user visits our application, pages are fully built (meaning, the HTML, CSS, and data are combined) on the server first and then sent to the client. While visually there's not much of a difference for the user—this helps us to omit the loading indicator on the client but is otherwise visually identical—introducing server-side rendering can significantly improve how an application is indexed by Google and other search engines. In some situations, too, server-side rendering can improve the page load time as well.

As of Meteor 1.5.1, the ability to implement server-side rendering has been made possible via the server-render package. In Pup, we've fully integrated this package making server-side rendering the default behavior. The following documentation explains how you can take advantage of server-side rendering in your app.

Usage of Meteor.isClient may be required

In order for SSR to work, we use the same exact components and routes that are used on the client. It's common for client-side code to rely on client-only features like the window value or built-in Meteor features like Meteor.subscribe(). If your code contains features like this, it will trigger an error on render. Keep an eye on your server console to see where to make use of Meteor.isClient.

Defining SSR Pages in Pup

Although SSR is fully integrated for you in Pup, we've avoided abstracting the individual components so that you can take full advantage of it. In particular, any page that relies on dynamic data loaded from your database will need to have its data loaded on the server via a Meteor Method and paired with a connection to the Redux store used to pass data from the server to the client.

Retrieving dynamic data for SSR

On the right, we've output the entirety of the file where server-side rendering occurs in Pup. Here, you'll see the onPageLoad() function that's invoked by the server-render package whenever an incoming HTTP request from a user is received.

As an example, the /documents/:_id route used for viewing a document in Pup has been wired up to leverage server-side rendering. Because we're not given anything but the path the user is trying to access in our application, we've defined a call to a helper module parseUrlForSSR() included with Pup which takes in the URL the user is trying to access (this is passed to us by the server-render package as part of the sink argument passed to the onPageLoad() function) and the expected topic we're trying to match.

Here, we know that we're trying to load a single document whose path is something like /documents/:_id so we pass 'documents' as the identifier we'd like the helper to match. This returns an object with two properties: isMatch and parts. The former is a true/false which tells us whether or not the current URL being accessed matches our expected topic. The latter, parts, gives us the individual parts of the path being accessed as an array.

If we look at the data object being defined below this, at the bottom we can see a property doc being added which makes use of this information. If we detect that we're visiting a documents-related page, we make a call to a Meteor Method, documents.findOne (we'll look at this next) which performs a .findOne() on the Documents collection in Pup to retrieve the document matching the _id we pass. The _id here is pulled from the parts array returned by parseUrlForSSR() and we expect the _id of the document in that URL to be at position 1 in the parts array, or, the second value in the array (e.g., for this URL the parts array should look something like ['documents', '97r4mwzhtWGGagSku']).

Defining a method for retrieving dynamic data

If we look at the /imports/api/Documents/methods.js file on the right, we can see how the documents.findOne() method is defined. First, we check to ensure that we've received the right type of data for documentId (because multiple paths could match /documents, we need to anticipate that the _id may be undefined), either a String or undefined.

From here, we'll attempt to find a document matching the documentId we've passed, returning back to the doc field on the data object inside of our /imports/startup/server/ssr.js file. From here, the data object is then placed into the Redux store we use to relay data to the client via createStore().

Final compilation of pages for SSR

Finally, back in /imports/startup/server/ssr.js, we work to compile the HTML and CSS (via styled-components), and SEO metadata into a string that can be injected into the <div id="react-root"></div> element located inside of /client/main.html. Finally, the data we placed into the Redux store is stringified and injected into the window object as __PRELOADED_STATE__. From here we can move to the client.

Completing the render on the client

Though the server part of our rendering is complete, we still need to ensure that the client can "see" the data we passed to it via our Redux store. The majority of the wiring for this has been completed for you in Pup, however, the React component that defines the page that's being SSR'd needs to load the data from Redux for this to work. To do it, we use the compose method from the redux package and connect from the react-redux package.

Looking at the example /imports/ui/pages/ViewDocument/ViewDocument.js on the right, at the bottom of the file, we can see export default compose() with two values passed to it: a call to connect() and another to withTracker(). The first is responsible for "connecting" the Redux store to our ViewDocument component. The idea here is that the doc value we defined earlier will now be accessible as a prop on the ViewDocument component (i.e., this.props.doc is the document we retrieved in our documents.findOne method).

If you've worked with Pup or Meteor and React before, the withTracker part here may look familiar. This function is used for pulling the same data we pulled via the documents.findOne method using Meteor's pub/sub system. At first glance this may seem redundant, but this is a neat tool for leveraging SSR and Meteor's reactive data.

By using the compose() method from Redux, we can join together the results of calling connect() and withTracker() together into a single object that's passed to our component—here, ViewDocument—as props. Using withTracker, after the page has loaded, if the data changes, we'll still be able to see the real-time changes without having to refresh the page.

SSR pages need to be public!

If you're just getting started with SSR, keep in mind that in order for it to work pages need to public. This means that inside of your /imports/ui/layouts/App/App.js file, any routes intended for server-side rendering need to be defined using the <Route /> component which does not check for a user before rendering the page.

When all is said and done, here's a look at what we can expect to happen at the browser level:

Server-side rendering example
Server-side rendering example

/imports/startup/server/ssr.js
/imports/api/Documents/methods.js
/* eslint-disable consistent-return */

import { Meteor } from 'meteor/meteor';
import { check, Match } from 'meteor/check';
import Documents from './Documents';
import handleMethodException from '../../modules/handle-method-exception';
import rateLimit from '../../modules/rate-limit';

Meteor.methods({
  'documents.findOne': function documentsFindOne(documentId) {
    check(documentId, Match.OneOf(String, undefined));

    try {
      return Documents.findOne(documentId);
    } catch (exception) {
      handleMethodException(exception);
    }
  },
  [...]
});
/imports/ui/pages/ViewDocument/ViewDocument.js

Adding SEO & Metadata

To aid in the process of adding SEO metadata to your server-side rendered pages, Pup includes a React component called <SEO /> which acts as a wrapper and template for the react-helmet package. Above, we can see that for each request, our SSR code attempts to call the Helmet.renderStatic() method and inject the meta tags it finds into the <head></head> of the page we're viewing.

Using the <SEO /> component, you can quickly and easily add the essential meta tags (we've pulled the list of tags in use from the popular SEO blog Moz), but also modify what's included for your own needs if you wish.

On the right, we can see the <SEO /> component as it's defined. Betwen the <Helmet></Helmet> tags, we can write normal HTML meta tags to describe our content. From here, React Helmet will inject these into the <head></head> of our document.

Below, we can see an example of putting the <SEO /> component to use:

<SEO
  title={doc.title}
  description={doc.body}
  url={`documents/${doc._id}`}
  contentType="article"
  published={doc.createdAt}
  updated={doc.updatedAt}
  twitter="clvrbgl"
/>
/imports/ui/components/SEO/SEO.js
/imports/ui/pages/ViewDocument/ViewDocument.js