Skip to content

Commit b709274

Browse files
committed
add document about custom dynamic title for better SEO
1 parent b438fab commit b709274

File tree

2 files changed

+93
-38
lines changed

2 files changed

+93
-38
lines changed

README.md

Lines changed: 92 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Yet another simple React SSR solution inspired by vue-server-render with:
88
2. prefetch/preload client injection with ClientManifest, generated by [ssr-webpack-plugin](https://github.com/JounQin/ssr-webpack-plugin)
99
3. server css support with [react-style-loader](https://github.com/JounQin/react-style-loader)
1010
4. Async component support with [react-async-component](https://github.com/ctrlplusb/react-async-component) and [react-async-bootstrapper](https://github.com/ctrlplusb/react-async-bootstrapper)
11+
5. custom dynamic head management for better SEO
1112

1213
## Real World Demo
1314

@@ -35,7 +36,7 @@ import { resolve } from './config'
3536

3637
import base from './base'
3738

38-
export default merge(base, {
39+
export default merge.smart(base, {
3940
// Point entry to your app's server entry file
4041
entry: resolve('src/entry-server.js'),
4142

@@ -64,6 +65,7 @@ export default merge(base, {
6465

6566
plugins: [
6667
new webpack.DefinePlugin({
68+
'process.env.REACT_ENV': '"server"',
6769
__SERVER__: true,
6870
}),
6971
// This is the plugin that turns the entire output of the server build
@@ -79,7 +81,8 @@ export default merge(base, {
7981
```js
8082
import webpack from 'webpack'
8183
import merge from 'webpack-merge'
82-
import HtmlWebpackPlugin from 'html-webpack-plugin'
84+
// do not need 'html-webpack-plugin' any more because we will render html from server
85+
// import HtmlWebpackPlugin from 'html-webpack-plugin'
8386
import { SSRClientPlugin } from 'ssr-webpack-plugin'
8487

8588
import { __DEV__, publicPath, resolve } from './config'
@@ -106,11 +109,9 @@ export default merge.smart(base, {
106109
},
107110
plugins: [
108111
new webpack.DefinePlugin({
112+
'process.env.REACT_ENV': '"client"',
109113
__SERVER__: false,
110114
}),
111-
new HtmlWebpackPlugin({
112-
template: resolve('src/index.pug'),
113-
}),
114115
new webpack.optimize.CommonsChunkPlugin({
115116
names: ['vendors', 'manifest'],
116117
}),
@@ -172,7 +173,7 @@ All you need to do is for hot reload on development:
172173

173174
Example: https://github.com/JounQin/react-hackernews/blob/master/server/dev.js
174175

175-
Your server bundle entry should export a function with a `userContext` param which return a promise, and it should resolve a react component instance.
176+
Your server bundle entry should export a function with a `context` param which return a promise, and it should resolve a react component instance.
176177

177178
Example: https://github.com/JounQin/react-hackernews/blob/master/src/entry-server.js
178179

@@ -184,17 +185,17 @@ Since you generate server bundle renderer as above, you can easily call `rendere
184185

185186
`renderToString` is very simple, just `try/catch` error to handle it.
186187

187-
`renderToStream` is a tiny complicated to handle, you can rediect or reject request by listening `error` event and handle error param. If you want to render application but change response status, you can listen `afterRender` event and handle with your own `userContext`, for example maybe you want to render 404 Not Found page via React Component but respond with 404 status.
188+
`renderToStream` is a tiny complicated to handle, you can rediect or reject request by listening `error` event and handle error param. If you want to render application but change response status, you can listen `afterRender` event and handle with your own `context`, for example maybe you want to render 404 Not Found page via React Component but respond with 404 status.
188189

189190
### State management
190191

191192
If you set `context.state` on server, it will auto inject a script contains `window.__INITIAL_STATE__` in output, so that you can resue server state on client.
192193

193-
### Style injection on server
194+
### Style injection and Head Management
194195

195196
Without SSR, we can easily use `style-loader`, however we need to collect rendered components with there styles together on runtime, so we choose to use [react-style-loader](https://github.com/JounQin/react-style-loader) which forked [vue-style-loader](https://github.com/vuejs/vue-style-loader) indeed.
196197

197-
Let's create a simple HOC for server style and http injection.
198+
Let's create a simple HOC for server style, title management and http injection.
198199

199200
```js
200201
import axios from 'axios'
@@ -203,38 +204,72 @@ import PropTypes from 'prop-types'
203204
import React from 'react'
204205
import { withRouter } from 'react-router'
205206

206-
export const withSsr = (styles, router = true) => Component => {
207-
class SsrConmponent extends React.PureComponent {
208-
static displayName = `Ssr${Component.displayName ||
209-
Component.name ||
210-
'Component'}`
207+
// custom dynamic title for better SEO both on server and client
208+
const setTitle = (title, self) => {
209+
title = typeof title === 'function' ? title.call(self, self) : title
211210

212-
static propTypes = {
213-
staticContext: PropTypes.object,
214-
}
211+
if (!title) {
212+
return
213+
}
215214

216-
componentWillMount() {
217-
// `styles.__inject__` will only be exist on server, and inject into `userContext`
218-
if (styles && styles.__inject__) {
219-
styles.__inject__(this.props.staticContext)
215+
if (__SERVER__) {
216+
self.props.staticContext.title = `React Server Renderer | ${title}`
217+
} else {
218+
// `title` here on client can be promise, but you should not and do not need to do that on server,
219+
// because on server async data will be fetched in asyncBootstrap first and set into store,
220+
// then title function will be called again when you call `renderToString` or `renderToStream`.
221+
// But on client, when you change route, maybe you need to fetch async data first
222+
// Example: https://github.com/JounQin/react-hackernews/blob/master/src/views/UserView/index.js#L18
223+
// And also, you need put `@withSsr` under `@connect` with `react-redux` for get store injected in your title function
224+
Promise.resolve(title).then(title => {
225+
if (title) {
226+
document.title = `React Server Renderer | ${title}`
220227
}
221-
}
228+
})
229+
}
230+
}
222231

223-
render() {
224-
return (
225-
<Component
226-
{...this.props}
227-
// use different axios instance on server
228-
http={__SERVER__ ? this.props.staticContext.axios : axios}
229-
/>
230-
)
231-
}
232+
export const withSsr = (styles, router = true, title) => {
233+
if (typeof router !== 'boolean') {
234+
title = router
235+
router = true
232236
}
233237

234-
return hoistStatics(
235-
router ? withRouter(SsrConmponent) : SsrConmponent,
236-
Component,
237-
)
238+
return Component => {
239+
class SsrConmponent extends React.PureComponent {
240+
static displayName = `Ssr${Component.displayName ||
241+
Component.name ||
242+
'Component'}`
243+
244+
static propTypes = {
245+
staticContext: PropTypes.object,
246+
}
247+
248+
componentWillMount() {
249+
// `styles.__inject__` will only be exist on server, and inject into `staticContext`
250+
if (styles.__inject__) {
251+
styles.__inject__(this.props.staticContext)
252+
}
253+
254+
setTitle(title, this)
255+
}
256+
257+
render() {
258+
return (
259+
<Component
260+
{...this.props}
261+
// use different axios instance on server to handle different user client headers
262+
http={__SERVER__ ? this.props.staticContext.axios : axios}
263+
/>
264+
)
265+
}
266+
}
267+
268+
return hoistStatics(
269+
router ? withRouter(SsrConmponent) : SsrConmponent,
270+
Component,
271+
)
272+
}
238273
}
239274
```
240275

@@ -250,7 +285,6 @@ import { withSsr } from 'utils'
250285

251286
import styles from './styles'
252287

253-
@withSsr(styles, false)
254288
@connect(
255289
({ counter }) => ({ counter }),
256290
dispatch => ({
@@ -259,6 +293,7 @@ import styles from './styles'
259293
decrease: () => dispatch(decrease),
260294
}),
261295
)
296+
@withSsr(styles, false, ({ props }) => props.counter)
262297
export default class Home extends React.PureComponent {
263298
static propTypes = {
264299
counter: PropTypes.number.isRequired,
@@ -297,7 +332,27 @@ export default class Home extends React.PureComponent {
297332
}
298333
```
299334

300-
Then `react-server-renderer` will automatically collect user styles on server and render them into output!
335+
And inside the template passed `title` to bundle renderer:
336+
337+
```html
338+
<html>
339+
<head>
340+
<title>{{ title }}</title>
341+
</head>
342+
<body>
343+
...
344+
</body>
345+
</html>
346+
```
347+
348+
Then `react-server-renderer` will automatically collect user styles and title on server and render them into output!
349+
350+
Notes:
351+
352+
* Use double-mustache (HTML-escaped interpolation) to avoid XSS attacks.
353+
* You should provide a default title when creating the context object in case no component has set a title during render.
354+
355+
Using the same strategy, you can easily expand it into a generic head management utility.
301356

302357
---
303358

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-server-renderer",
3-
"version": "0.2.2",
3+
"version": "0.2.3",
44
"description": "simple React SSR solution inspired by vue-server-render",
55
"repository": "git@github.com:JounQin/react-server-renderer.git",
66
"main": "lib/index.js",

0 commit comments

Comments
 (0)