Authors:

Inna Ivashchuk

Volodymyr Vyshko

Volodymyr Vyshko

Inna Ivashchuk

Plan

  • How does search crawler work?
  • How to use SEO power
  • Typical troubles with SEO in SPA
  • Live demo about Server Side rendering
  • Q&A session

How does search crawler work?

How do pages get into Search ?

This is Googlebot

How Googlebot works...

Crawler

Queue

Crawler

URL

Procesing

Index

Ranking

Render

Queue

Renderer

HTML

 Rendered HTML

URLs

Good news

but not right away

How to use SEO power?

Tip 1

Content matters

Understandable and informative content 

Nice UI/UX

Valid HTML with semantic elements

<html>
    <head>
	<title>My super HTML</title>    
    </head>
    <body>
        <header> 
    	    <nav> 
    	        <ul><li><a href="/home">Home</a></li></ul>
    	    </nav>
    	    <h1>Page title</h1>
    	</header>
    	<main> 
    	    <article>Nice article can be there</article>
    	    <section>
    	        <form><input type="email" name="email"></form>
    	    </section>
    	</main>
    	<footer> </footer>
    	</body>
</html>

HTML <meta> Tags

<head>
  <meta charset="UTF-8">
  <meta name="description" content="Star Wars slides about SEO">
  <meta name="keywords" content="HTML,CSS,SEO,JavaScript,Vader,Luke,R2D2">
  <meta name="author" content="Master Yoda">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>

   The <meta> tag provides metadata about the HTML document. Metadata will not be displayed on the page, but will be machine parsable.

    Meta elements are typically used to specify page description, keywords, author of the document, last modified, and other metadata.

    The metadata can be used by browsers (how to display content or reload page), search engines (keywords), or other web services.

Readable and meaningful URLs only

Tranditional Urls

  • starwars.com/traditional-url
  • starwars.com/page#section

Tricking the browser with fragment identifiers

  • starwars.com/#about
  • starwars.com/#!about

 

Best practice? Use History API

  • starwars.com/home

 

<a href="/home" onClick={doSomething}>Home</a>

Don't forget about semantic elements details

<h1>Characters in Star Wars</h1>
<p>
    There is you can find the collection of the most powerful characters of Star Wars
</p>
<img src="/img/jedi.jpg" alt="Super powerful Jedi">
<img src="/img/sith.jpg" alt="Super powerful Sith Lord">

Tip 2

Give a content as quickly as possible

Take care about page loading 

Check page load speed

Run Lighthouse in Chrome DevTools

Tip 3

Make your content stand out

Provide descriptive title

Every page should have unique description

Typical troubles with SEO in SPA

SPA without JavaScript

<!DOCTYPE html>
<html lang="en">
<head>
    <title>StarWars | The Official Star Wars Website</title>
    <link rel="stylesheet" href="https://static.disney.io/starwars.css" type="text/css">
</head>
<body>
    <div id="content"></div>
    <script src="https://static.disney.io/main.min.js"></script>
</body>
</html>

Main troubles in SPA

  • One title for all pages
  • One collection of meta tags
  • Content is missed (text, images)
  • URLs to sub-pages are not rendered

React SEO basics with Helmet

import React from 'react';
import { Helmet } from 'react-helmet';

class MainPage extends React.Component {
    // ...
    render() {
        return (
            <div>
                // ...
                <Helmet>
                    <title> unique, descriptive title </title>
                    <meta name="description" content="unique, helpful snippet" />
                </Helmet>
            </div>
        )
    }
} 

export default MainPage;

Live demo

Server Side Rendering

Text

import express from 'express';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import { StaticRouter } from 'react-router';
import { Helmet } from 'react-helmet';
import { ServerStyleSheet } from 'styled-components';

import App from './src/app';

Server imports

    const sheet = new ServerStyleSheet();

    const content = ReactDOMServer.renderToString(
        sheet.collectStyles(
            <StaticRouter location={req.url} context={{}}>
                <App />
            </StaticRouter>
        )
    );

    const helmet = Helmet.renderStatic();
    res.send(`
        <html>
            <header>
                ${helmet.title.toString()}
                ${helmet.meta.toString()}
                ${sheet.getStyleTags()}
            </header>
            <body>
                <div id="root">
                    ${content}
                </div>
                <script src="client_bundle.js"></script>
            </body>
        </html>    
    `)

Render application on server

Webpack configuration

const path = require('path');

module.exports = {
  entry: './src/client.js',
  output: {
    filename: 'client_bundle.js',
    path: path.resolve(__dirname, 
                    'build/public'),
    publicPath: '/build/public',
  },
  module: {
    rules: [{
      test: /\.(js|jsx)$/,
      exclude: /node_modules/,
      use: ['babel-loader']
    }]
  },
}
const path = require('path');
const webpackNodeExternals = 
    require('webpack-node-externals');

module.exports = {
  target: 'node',
  entry: './server.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'build'),
    publicPath: '/build',
  },
  module: {
    rules: [{
      test: /\.(js|jsx)$/,
      exclude: /node_modules/,
      use: ['babel-loader']
    }]
  },
  externals: [webpackNodeExternals()]
}

Client

Server

    "webpack:server": "webpack --config webpack.server.js --watch",
    "webpack:client": "webpack --config webpack.client.js --watch",
    "webpack:start": "nodemon --watch build exec node build/bundle.js",
    "dev": "npm-run-all --parallel webpack:*"

package.json

nodemon - is a tool that helps develop node.js based applications by automatically restarting the node application when file changes in the directory are detected.

npm-run-all - a CLI tool to run multiple npm-scripts in parallel or sequential.

Dependencies

hydrate() - Same as render(), but is used to hydrate a container whose HTML contents were rendered by ReactDOMServer. React will attempt to attach event listeners to the existing markup.

ReactDOM.hydrate(element, container[, callback])

React expects that the rendered content is identical between the server and the client. It can patch up differences in text content, but you should treat mismatches as bugs and fix them. In development mode, React warns about mismatches during hydration.

Render application on client

Results

Dynamic rendering

export const botUserAgents = [
  'googlebot',
  'google-structured-data-testing-tool',
  'bingbot',
  'linkedinbot',
  'mediapartners-google',
];
isPrerenderedUA = userAgent.matches(botUserAgents)
isMobileUA = userAgent.matches(['mobile', 'android'])


if (!isPrerenderedUA) {
    ...
} else {
  servePreRendered(isMobileUA)
}
app.get('/', function(req, res) {
    console.log('User-Agent: ' + req.headers['user-agent']);
});

Get user-agent:

Render only for bot:

Redux

import { renderToString } from 'react-dom/server'

function handleRender(req, res) {
  // Create a new Redux store instance
  const store = createStore(counterApp)

  // Render the component to a string
  const html = renderToString(
    <Provider store={store}>
      <App />
    </Provider>
  )

  // Grab the initial state from our Redux store
  const preloadedState = store.getState()

  // Send the rendered page back to the client
  res.send(renderFullPage(html, preloadedState))
}

Handling the Request

Inject Initial Component HTML and State

renderFullPage(html, preloadedState) {
  return `
    <!doctype html>
    <html>
      <head>
        <title>Redux Universal Example</title>
      </head>
      <body>
        <div id="root">${html}</div>
        <script>
          // WARNING: See the following for security issues around embedding JSON in HTML:
          // http://redux.js.org/recipes/ServerRendering.html#security-considerations
          window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState).replace(
            /</g,
            '\\u003c'
          )}
        </script>
        <script src="/static/bundle.js"></script>
      </body>
    </html>
    `
}

The Client Side

import React from 'react'
import { hydrate } from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import App from './containers/App'
import counterApp from './reducers'

// Grab the state from a global variable injected into the server-generated HTML
const preloadedState = window.__PRELOADED_STATE__

// Allow the passed state to be garbage-collected
delete window.__PRELOADED_STATE__

// Create Redux store with initial state
const store = createStore(counterApp, preloadedState)

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

Things to keep in mind

  • componentDidMount is not called on the server — this means that none of your data-fetching that you’re used to place there will not be called
  • state generated on the server side will not be passed to the client application state
  • to make router work you need to make sure that the proper URL is passed to the application when it’s rendered on the server

Whats next ?

Gatsby, next.js  - Two of the most popular solutions that provide SSR for React

Demo project source code:

Thank You

Presentation can be found:

Q&A

May the SPA be SEO optimized

By Inna Ivashchuk

May the SPA be SEO optimized

SEO, crawlers, and SSR with Next

  • 665