+3

SEO Friendly Single Page Application: Server Rendering With React

Getting Start

As a lot of new technologies are increasing, web application development has been change a great deal. And amongst those, Single Page Application has been increasingly popular. SPA offers very dynamic and attractive UI which makes great for user experiece. Also thanks to its dynamic and partially update nature makes it seems like everything load faster than before. But as good as it sounds, SPA also has its down fall. One is the long initial loading time, because browser need to download large application code and assets all at once. Another is SEO which is a major problem. Due to its dynamic feature that push responsibility of rendering markup from server to client makes SPA not SEO friendly. There is a technique that can work around this issue and that is the subject of this article. Before we start I want to let you know that I won't go into details on how Redux works as it is not meant to be for this article, but you can find the full documentation here. As always you can get access to the full source code from my repository here.

Server Rendering to The Rescue

The goal is to fool search engine crawler or spider or robot or whatever you call it, by providing the initial html markup whenever it tries to crawl our page and add the ability to reuse that markup on the client side as the starting point of SPA. Well this might sounds complicated but trust me it's not all that mystery, once you understand the concept behind it, you'll see that it is easy to implement.

There are a lot of frameworks that can be used to build SPA, but I'm going to pick React along with Redux, a Flux like architecture. There might be different approach between each framework but the core idea is the same. As for server side I'm gonna go with Node.js as it's easy to get up and running. Along with this, the code will be writen in ES6 and use webpack as a build tool for module bundling. Now that everything is set let's get started, shall we?

Server

I'll go through fllowing code real quick and point out the main point that neccessary in order for server rendering to work. First I created an express application and add a callback to listen to user request. Next in the callback I use webpack json loader to load data needed to render html markup. After that I created a Redux store and dispatch a loadAnimes action passing in above data as a payload. These two lines of code are important in order to fill application state. Reducer are functions that perform application logic and calculation on data that came from a certain action when there is new data flow in application store. At this stage if everything finished without a problem (which it should be), store will now contains data load from json file. On the next line I used ReactDOM.renderToString, which as its name suggests, render App component into string. Take note on Provider component which reveive previous store as property. Every children of this component will now has access to Redux store. Now that the initial html markup is ready, the next step is to get initial state for client side app. This can be achieve by making a call to store.getState() and JSON.stringify() and attach it as __PRELOAD_STATE__ property to window object so that it will be available to be used on browser. All that is left is to send this generated string as a response to client. This is done for the half part, the next question is how do we attach this markup back on client side so that we can reuse what was send from the server.

import React from 'react';
import App from './app/app';
import express from 'express';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import appReducer from './app/reducer';
import { loadAnimes } from './app/action';
import { renderToString } from 'react-dom/server';

const app = express();
app.use(express.static(`${__dirname}/public`));
app.use('*', (req, res, next) => {
  const animes = require('./animes.json');
  const store = createStore(appReducer);
  store.dispatch(loadAnimes(animes));

  const body = renderToString(
    <Provider store={store}>
      <App />
    </Provider>
  );

  const preloadState = store.getState();

  const html = `
    <!doctype html>

    <html>
      <head>
        <title>SEO Friendly App</title>
      </head>
      <body>
        <div id="root">${ body }</div>
        <script>
          window.__PRELOAD_STATE__ = ${JSON.stringify(preloadState)};
        </script>
        <script src="/assets/app.js"></script>
      </body>
    </html>
  `

  res.send(html);
});

app.listen(8080);

Client

Provided that you have clone and run the sample app on the browser, if you open up the source of the page you'll notice that the markup that generated by renderToString will have a react id attach to it. React will use that existing id to find and attach the markup into virtual dom tree when we try to mount new component on user browser as a result BOOM!, we just reuse the markup from the server as a starting point. But hey! what about the application state. Well, recall that we've attach the initial state object to window.__PRELOAD_STATE__ in our previous code right? then all we have to do now is to use that to fill our application state. Turns out that createStore let us do just that by pass in the initial state as second argument to function call. The rest is to mount App component the same way we do on server side then we are done. Take a look at the following code snippet.

import React from 'react';
import { render } from 'react-dom';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import App from './app';
import appReducer from './reducer';

const preloadState = window.__PRELOAD_STATE__;
const store = createStore(appReducer, preloadState);

render(
  <Provider store={ store }>
    <App />
  </Provider>
, document.getElementById('root'));

The following code are shared between server side and client side

// App component
import AnimeList from './animeList';
import { connect } from 'react-redux';
import React, { PropTypes } from 'react';

class App extends React.Component {
  static propTypes = {
    animes: PropTypes.array.isRequired
  }

  render() {
    const { animes } = this.props;

    if (animes.length === 0) {
      return null;
    } else {
      return (<AnimeList animes={ animes } />);
    }
  }
}

export default connect(
  (state) => state,
  { }
)(App);

// AnimeList component
import React from 'react';

export default class AnimeList extends React.Component {
  render() {
    const animeNodes = this.props.animes.map((anime) => {
      return (
        <a href="#" key={ anime.id }>
          <div className="image"><img src={ anime.image } /></div>
          <p>{ anime.title }</p>
        </a>
      );
    });

    return (<div className="animes">{ animeNodes }</div>);
  }
}

// appReducer
import { combineReducers } from 'redux';

const animes = (state = [], action) => {
  switch (action.type) {
    case 'ANIMES_LOADED':
      return action.animes;
    default:
      return state;
  }
}

export default combineReducers({ animes });

Conclusion

Congratulation you are now have a technique in your toolbox to build SEO friendly Single Page Application that will impresses your user and your client.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí