SSR React with Symfonyfor the Strong of Spirit Workshop

Meta tags

Theory
Let's talk about providing meta tags.

Titles

We can set titles and other meta tags in React using React Helmet. Let's install it:
npm i react-helmet
We can use it to set the titles of the pages dynamically. Edit assets/js/MovieList.js and add an import and change its render method:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import Helmet from "react-helmet";

//...

render() {
  if (!this.state.movies) {
    return "loading...";
  }

  return (
    <div>
      <Helmet>
        <title>Bond movies</title>
      </Helmet>
      {this.state.movies.map(movie => (
        <Link to={`movie/${movie.slug}`} key={movie.slug}>
          <MovieComponent movie={movie} />
        </Link>
      ))}
    </div>
  );
}
And now assets/js/MovieDetail.js:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import Helmet from "react-helmet";

//...

render() {
  if (!this.state.movie) {
    return "Loading...";
  }
  return (
    <div>
      <Helmet>
        <title>{this.state.movie.name}</title>
      </Helmet>
      <Link to="/">Back to list</Link>
      <MovieComponent movie={this.state.movie} />
    </div>
  );
}
This works in the browser (you should see the link in your browser), but if you open the source code of the page, the title is not there, so this won't work with crawlers that only read HTML, such as some of the Google crawlers and the Facebook crawler for OG: tags.
Let's provide a solution for SSR in app/js/MoviesApp.js:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import Helmet from "react-helmet";
import { renderToString } from "react-dom/server";

//...

export default (initialProps, context) => {
  if (context.serverSide) {
    return {
      renderedHtml: {
        componentHtml: renderToString(
          <StaticRouter
            basename={context.base}
            location={context.location}
            context={{}}
          >
            <MainApp {...initialProps} />
          </StaticRouter>
        ),
        title: Helmet.renderStatic().title.toString()
      }
    };
  } else {
    return (
      <BrowserRouter basename={context.base}>
        <MainApp {...initialProps} />
      </BrowserRouter>
    );
  }
};
And now change templates/movies/detail.html.twig and templates/movies/list.html.twig
1
2
3
4
5
6
7
8
9
10
{% extends 'base.html.twig' %}

{% set movie = react_component_array('MoviesApp', {'props': props}) %}
{% block title %}
{{ movie.title is defined ? movie.title | raw : '' }}
{% endblock title %}

{% block body %}
{{ movie.componentHtml | raw }}
{% endblock %}
If you want to unify them and make the controllers render the same template (since they are identical, you are more than welcome).
Got lost?
The code up to this point is in the tag 05-meta of the repository https://github.com/Limenius/workshop-symfony-react.git
This means:
git reset HEAD --hard
(To discard your changes)
And then:
git checkout 05-meta