Walkthrough
HotStone Client is a Javascript library used by server-side-rendering (SSR) web app to get HTML tag data for every requesting page. Then, HTML tag data can be rendered with React Helmet.
This page will guide you to initiate a simple SSR web app and use HotStone Client upon it. If you already have a SSR web app, you can jump to HotStone Client Usage to know how to use HotStone Client.
Or, you can read full source code that is availabe on https://github.com/hotstone-seo/hotstone-workshop
Initiate A Simple SSR Web App
We will use Create React SSR App to easily setup a React SSR app.
$ npx create-react-ssr-app ssr-app
$ cd ssr-app
$ npm install
$ npm install react-helmet serialize-javascript
Modify src/server/index.js
to get a bare minimum SSR app that ready to integrate with HotStone Client
// src/server/index.js
import path from 'path';
import express from 'express';
import React from 'react';
import { Helmet } from 'react-helmet';
import ReactDOMServer from 'react-dom/server';
import serialize from 'serialize-javascript';
import App from '../App';
const publicPath = path.join(__dirname, '/public');
const app = express();
app.use(express.static(publicPath));
const template = ({ body, helmet }, data) => {
return `
<!DOCTYPE html>
<html ${helmet.htmlAttributes.toString()}>
<head>
${helmet.title.toString()}
${helmet.meta.toString()}
${helmet.link.toString()}
${helmet.script.toString()}
</head>
<body ${helmet.bodyAttributes.toString()}>
<div id="root">${body}</div>
<script>window.__data = ${serialize(data)}</script>
</body>
</html>
`
}
app.get('*', (req, res, next) => {
(async function() {
try {
const data = {}
const appString = ReactDOMServer.renderToString(<App />);
const helmet = Helmet.renderStatic();
res.send(template({ body: appString, helmet: helmet }, data));
} catch(error) {
next(error);
}
})();
});
export default app;
For convenience, let’s remove big React logo at src/App.js
// src/App.js
import React from 'react';
// import logo from './logo.svg';
import './App.css';
function App() {
return (
<div className="App">
<header className="App-header">
{/* <img src={logo} className="App-logo" alt="logo" /> */}
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;
Run SSR web app:
$ npm run start
If it’s successfully running, open http://localhost:3000
HotStone Client Usage
Install HotStone Client
$ npm install hotstone-client
# to use 'renderHelmetTags'
$ npm install react
On Server-Side
To initiate HotStoneClient
, it requires two mandatory arguments and last optional argument:
// src/server/index.js
import { HotStoneClient } from 'hotstone-client'
// ...
const hotstoneURL = 'http://localhost:8089'
const clientKey = 'Otdu1qe.A5eKbNQ3Kj26kiTQmwWAmzaQGk5uYmSI'
const fetchOpts = { cacheManager: `./hotstone-local-cache` } // enable local cache
const client = new HotStoneClient(hotstoneURL, clientKey, fetchOpts)
// ...
- (mandatory)
hotstoneURL
: URL of HotStone Provider - (mandatory)
clientKey
: To interact with HotStone Provider, it needs client key. Get client key from HotStone UI / Dashboard on menuClient Keys
- (optional)
fetchOpts
: Under the hood, HotStone Client uses make-fetch-happen. We can pass extra fetch options to get additional features, i.e. local caching with{ cacheManager: './hotstone-local-cache' }
HotStone Client has two main functions :
match(<path>)
: retrieve SEO rule data of a page by matching its request page path.tags(<rule>, <locale>)
: retrieve HTML tag data associated with given rule and locale.
Let’s call these functions on src/server/index.js
// src/server/index.js
import { HelmetWrapper } from '../hotstone'
// ...
app.get('*', (req, res, next) => {
(async function () {
try {
const rule = await client.match(req.path);
const tags = await client.tags(rule, "en_US");
const data = { tags }
const appString = ReactDOMServer.renderToString(
<HelmetWrapper tags={tags}>
<App />
</HelmetWrapper>
);
const helmet = Helmet.renderStatic();
res.send(template({ body: appString, helmet: helmet }, data));
} catch (error) {
next(error);
}
})();
});
// ...
data
that contains tags
also passed on to window.__data
HelmetWrapper
If you notice, there is a wrapper component that receives tags
. This component simply renders tags
as HTML tag using renderHelmetTags
provided by HotStone Client ('hotstone-client/lib/react'
).
Remember, HelmetWrapper
is not part of HotStone Client package. It’s just a sample component to demonstrate how to use renderHelmetTags
with React Helmet.
// src/hotstone/index.js
import React from 'react'
import Helmet from 'react-helmet'
import { renderHelmetTags } from 'hotstone-client/lib/react';
class HelmetWrapper extends React.Component {
constructor(props) {
super(props);
const { tags } = props;
this.state = { tags };
}
render() {
const { tags } = this.state;
return (
<>
<Helmet>{renderHelmetTags(tags)}</Helmet>
{this.props.children}
</>
);
}
}
export { HelmetWrapper }
On Client-Side
Hydrate your component that uses renderHelmetTags
(HelmetWrapper
in this case).
// src/client/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { HelmetWrapper } from '../hotstone'
import './index.css';
import App from '../App';
ReactDOM.hydrate(
<HelmetWrapper tags={window.__data.tags}>
<App />
</HelmetWrapper>,
document.getElementById('root')
);