Want to be More Productive? Checkout my Open Source Productivity Application

Building a Static Website Using React JS: Part #3 Adding Meta Tags and Generating HTML Pages

In this tutorial we will continue from where we left of in Part 2.

You can checkout Part #1 of this tutorial here Building a Static Website Using React JS: Part #1 Project Setup and Website UI

Before We Continue

Adding Meta tags is not as Straightforward, Because we have to add it in our React Components (pages) and also in the HTML pages that will be generated (pre-rendered).

Meaning we will be adding same content at multiple places., It will be best to keep all of our META tags in a separate file and use that file wherever we want, So in future when we want to change the meta tags content or add new meta tags We will only have to add/change it at a single location.

Also there seems to be an issue with the Slider we're using in the Homepage, The issue causes HTML file generation to fail, Right now the easiest fix is to remove the Slider from homepage, I will remove the slider for now, But we will get back to this later.

Let's edit the file src/content/Home.js and replace its content with

'use strict'; import React from 'react'; import DefaultLayout from 'app/layouts/Default'; import { Heading } from 'app/components/UI'; import { Row, Col, Carousel } from 'antd'; const Home = (props) => { return ( <DefaultLayout> <Heading title="Hey You, Yes You!, Want to be More Productive? Have lists of things you care about? Love simple and sexy UI?" /> <Col span={24} className="component--slider"> <div> <div className="image"> <img src="/images/slider/1.png" /> </div> <div className="title" style={{ textAlign: 'center' }}>This is a screenshot of the Board view page</div> </div> </Col> </DefaultLayout> ); } export default Home;

This will fix the issue. Hope this all made sense., Let's continue.

Step#1: Let's Create Our Meta.js File

This file will hold all meta related information for our pages (and in future our article information as well).

So which and all tags will we be adding to our pages?, Ofcourse the common ones like title, description and keywords.

In future when we add blog section, We will add more tags, but this should be enough for now., If you want to add more tags NOW just add it here.

Create a new file named src/meta.js and add this content in it

'use strict'; module.exports = [ // specify the default tags for all the routes here. // this will be used if the tags for given url is not specified here. { url: 'default', title: 'Productivity Application - Kanban Style Customizable Boards, Lists and Cards to make you more productive.', description: 'Kanban style, Trello inspired Productivity application built using the awesome React, Ant Design, Apollo Client and other fantastic libraries.', keywords: 'productivity application, productive, tasks, boards, cards, todo list, card sharing, boards sharing, darg drop lists', }, { url: 'about', title: 'About | Productivity Application', description: 'General information about this productivity application, How to install and use it.', keywords: 'productivity application about, about', }, { url: 'features', title: 'Features | Productivity Application', description: 'These are some of the Features of this application.', keywords: 'features, productivity app features, application features, productivity application', }, { url: 'contact-us', title: 'Contact Us | Productivity Application', description: 'Want to get in touch with us? Enter the form below.', keywords: 'productivity app, contact, get in touch', }, ];

This file exports a javascript Array and that array contains items with fields url, title, description and keywords

url will be used to map these tags to the urls in our website. Object with url 'default' will be used incase a route doesn't match any of the objects present in this file.

Form now on, Whenever we need a new tag, We will add it in this file.

Step#2: Let's Add Meta Tags to Our Pages

We will make use of react-helmet Package and Add meta tags to our pages.

If you open the website now and see all of our page, You will see same title and description content for all pages.

One way to easily add different tags to different pages in our website in a React Friendly way is to use React Helmet library.

Let's create a new component which will take care of adding all the SEO tags to our pages. Create a file named src/components/SEO.js and add this code in it.

'use strict'; import React from 'react'; import Meta from 'app/meta'; import { Helmet } from "react-helmet"; import _ from 'lodash'; const SEO = (props) => { let content = _.find( Meta, { url: props.url } ); if ( ! content ) { content = _.find( Meta, { url: 'default' } ); } return ( <Helmet> <title>{ content.title }</title> <meta name="description" content={ content.description } /> <meta name="keywords" content={ content.keywords } /> </Helmet> ); } export default SEO;

Now open each and every Page Component and import this SEO Component and specify the url property on this component as the current page url.

Open terminal and run the command yarn watch So we can see the changes as we make them.

Let's open file src/content/Home.js and replace its code with

'use strict'; import React from 'react'; import DefaultLayout from 'app/layouts/Default'; import { Heading } from 'app/components/UI'; import { Row, Col, Carousel } from 'antd'; import SEO from 'app/components/SEO'; const Home = (props) => { return ( <DefaultLayout> <Heading title="Hey You, Yes You!, Want to be More Productive? Have lists of things you care about? Love simple and sexy UI?" /> <Col span={24} className="component--slider"> <div> <div className="image"> <img src="/images/slider/1.png" /> </div> <div className="title" style={{ textAlign: 'center' }}>This is a screenshot of the Board view page</div> </div> </Col> <SEO url="home" /> </DefaultLayout> ); } export default Home;

All I have done is import the SEO component and I have placed this component <SEO url="home" /> at the bottom. You can place this component anywhere you want as it wont affect the page content or its layout but only its META Tags.

You can notice I have added a property on SEO component named url, This is the URL of the page where we included this component, Since no object with url home exists in our src/meta.js file, Default tag content will be used.

Now open file named src/content/About.js and add this content in it

'use strict'; import React from 'react'; import DefaultLayout from 'app/layouts/Default'; import { Heading, URL } from 'app/components/UI'; import { Row, Col } from 'antd'; import SEO from 'app/components/SEO'; const About = (props) => { return ( <DefaultLayout> <Heading title="Productivity Application - Kanban Style Customizable Boards, Lists and Cards to make you more productive." subtitle="Kanban style, Trello inspired Productivity application built using the awesome React, Ant Design, Apollo Client and other fantastic libraries." /> <Col span={14} offset={5} style={{ marginTop: 40 }}> <p>For installation instructions and how to use this application, Please visit <URL to="https://github.com/dhruv-kumar-jha/productivity-frontend" /></p> </Col> <SEO url="about" /> </DefaultLayout> ); } export default About;

Again in this file, We just imported our SEO component, and called it with its url as about.

Please do this for the remaining pages src/contentFeatures.js and src/ContactUs.js, If you run into any issue just checkout the Repository of this project.

Now when you navigate between pages you can see the new title for these pages reflected., However if you see the source code of the page, You will see the default code of public/200.html file. This is no good.

Step#3: Setup Before Generating HTML Pages for Our Routes

Now we want to pre-render html pages for all of our routes with correct content and Meta tags

Pre-rendering is not very easy if you're not using a server and this complicates things a little, but little work is worth the results.

For pre-rendering we will need two files

routes
This file will contain all of our routes information
template
This template file will contain our HTML structure.

Since we're making use of Code Splitting we cannot make of our existing src/routes.js file and sadly we will have to create another file with exact same routes without the dynamic imports.

Little time consuming yes, But there's no other way at the moment. (If you find any other way let me know and i will update the article.)

Le't create a new file named src/static/routes.js and src/static/template.js

Always remember to add the route in your src/routes.js as well as src/static/routes.js otherwise html file won't be generated for that route., Our website will still work though.

Open the file src/static/routes.js and add this code it.

'use strict'; import React from 'react'; import { Route, IndexRoute } from 'react-router'; import Home from 'app/content/Home'; import Features from 'app/content/Features'; import About from 'app/content/About'; import ContactUs from 'app/content/ContactUs'; import PageNotFound from 'app/content/PageNotFound'; const routes = ( <Route path='/'> <IndexRoute component={ Home } /> <Route path='features' component={ Features } /> <Route path='about' component={ About } /> <Route path='contact-us' component={ ContactUs } /> <Route path='*' component={ PageNotFound } /> </Route> ); export default routes;

Open the file src/static/template.js and add this code it.

'use strict'; import React from 'react'; import _ from 'lodash'; import Meta from '../meta'; const HTML = ( props ) => { const body = props.body; // lets find the file names for vendor, bundle and manifest file from the manifest object provided to us by the react-static-webpack-plugin const vendor = _.find( props.manifest, (file) => { return _.includes(file, 'vendor'); }); const bundle = _.find( props.manifest, (file) => { return _.includes(file, 'bundle'); }); const manifest = _.find( props.manifest, (file) => { return _.includes( file, 'manifest' ); }); // let's find the name of the url for which the html file is being generated // we will use that to find its meta details. let url = ''; let urls = props.reactStaticCompilation.renderProps.location.pathname; urls = urls.split('/'); if ( urls.length == 2 && urls[1] == '' ) { url = 'home'; } else { if ( urls.length == 2 ) { url = urls[1]; } else { url = urls[2]; } } let content = _.find( Meta, { url: url } ); if ( ! content ) { content = _.find( Meta, { url: 'default' } ); } return ( <html lang='en'> <head> <meta charSet="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> <meta httpEquiv="X-UA-Compatible" content="IE=edge" /> <title>{ content.title }</title> <meta name="description" content={ content.description } data-react-helmet="true" /> <meta name="keywords" content={ content.keywords } data-react-helmet="true" /> <link rel="icon" href="/images/favicon.ico" /> <link href="/styles.css" rel="stylesheet" /> </head> <body> <div id='root'> <div dangerouslySetInnerHTML={{ __html: body }} /> </div> <script defer src={`/${ manifest }`} /> <script defer src={`/${ vendor }`} /> <script defer src={`/${ bundle }`} /> </body> </html> ); } export default HTML;

If your yarn watch (webpack watch) command is still running stop it by pressing ctrl + c twice.

Open the file webpack.config.js and add this code in the plugins array.

new ReactStaticPlugin({ routes: './src/static/routes.js', template: './src/static/template.js', }),

If you now run the command yarn watch you will see new html files created for all the routes with correct content and meta tags.

If you encounter any error, checkout the Code Repository on GitHub or let me know.

Step#4: Minifying our Generated Assets

Everything is fine, Except for our javascript file sizes.

It's time we minify everything, This will save space, decrease script file size and will be faster to load.

Let's also create gzip versions of our file, So wherever we host our website, If gzip is supported, The gzipped files will be used, That will save even more on file size and bandwidth.

Open webpack.config.js and replace the entire code with

'use strict'; const webpack = require('webpack'); const path = require('path'); const BUILD_DIR = path.resolve(__dirname, 'public/scripts'); const APP_DIR = path.resolve(__dirname, 'src'); const PUBLIC_DIR = path.resolve(__dirname, 'public'); const ExtractTextPlugin = require("extract-text-webpack-plugin"); const HTMLWebpackPlugin = require('html-webpack-plugin'); const CompressionPlugin = require("compression-webpack-plugin"); const ReactStaticPlugin = require('react-static-webpack-plugin'); // put all the code of these packages in a single file const VENDOR_LIBS = [ 'react', 'react-dom', 'react-router', 'react-helmet' ]; const WebpackConfig = { entry: { bundle: APP_DIR + '/app.js', // this is our entry file vendor: VENDOR_LIBS // specifying which and all libraries the vendor file will contain }, output: { path: PUBLIC_DIR, // output directory // place the generated files in /public/scripts directory and name them accordingly. filename: 'scripts/[name].[chunkhash].js', // chunk files, place them in /public/scripts directory and name them accordingly. chunkFilename: 'scripts/[name].[chunkhash].chunk.js', // path where the generated code chunks will be, in our case its the same dir. publicPath: '/', }, // include all these modules. // these modules will compile our code so we can use it in the browser. module: { rules: [ { enforce: 'pre', test: /.js$/, exclude: /node_modules/, loader: 'eslint-loader', include : APP_DIR }, { loader: 'babel-loader', test: /.js$/, exclude: /node_modules/, include : APP_DIR, options: { presets: [ ['env',{ modules: false }], 'react' ], plugins: [ 'lodash', [ 'import', { libraryName: 'antd', style: 'css' } ], 'syntax-dynamic-import' ] } }, { test: /.scss$/, use: ExtractTextPlugin.extract({ use: ['css-loader', 'sass-loader'] }), }, { use: ExtractTextPlugin.extract({ use: 'css-loader', }), test: /.css$/ }, { loader: 'json-loader', test: /.json$/ } ], }, // load all these plugins plugins: [ new ExtractTextPlugin({ filename: 'styles.css', allChunks: true }), new webpack.optimize.CommonsChunkPlugin({ names: ['vendor', 'manifest'], minChunks: Infinity, }), new HTMLWebpackPlugin({ inject: false, filename: '200.html', template: 'scripts/200.ejs', minify: { collapseBooleanAttributes: true, removeComments: true, collapseWhitespace: true, } }), process.env.NODE_ENV !== 'production' ? () => {} : new webpack.optimize.UglifyJsPlugin({ beautify: false, mangle: { screw_ie8: true, // keep_fnames: true }, sourceMap: false, compress: { warnings: false, screw_ie8: true }, comments: false }), process.env.NODE_ENV !== 'production' ? () => {} : new ReactStaticPlugin({ routes: './src/static/routes.js', template: './src/static/template.js', }), process.env.NODE_ENV !== 'production' ? () => {} : new webpack.optimize.AggressiveMergingPlugin(), process.env.NODE_ENV !== 'production' ? () => {} : new CompressionPlugin({ asset: "[path].gz[query]", algorithm: "gzip", test: /.(js|html)$/, threshold: 10240, minRatio: 0.8 }), ], // this will resolve the path in the components we will write // this will help us get rid of the ugly Ex: '../../filetoinclude' syntax and we can just say app/file to import it. resolve: { alias: { app: APP_DIR, public: PUBLIC_DIR, }, extensions: [ '.js', '.json' ] }, }; module.exports = WebpackConfig;

Also, You can see our public/scripts directory has lot of files and we will have to manually delete them everytime, Let's install a new package that will help us delete this directory automatically.

Run the command yarn add --dev rimraf this will install rimraf package.

Now, replace the contents of package.json scripts object with this.

"scripts": { "start": "node scripts/server.js", "clean": "rimraf public/scripts", "watch": "cross-env NODE_ENV=development webpack -w", "build": "npm run clean && cross-env NODE_ENV=production webpack -p", "deploy": "firebase deploy" }

Finally run the command yarn build and all of our assets and html files will be generated, It will be minified as well as have their own gzip versions.

Run the command yarn start and you should see the website with minified assets and html files pre-rendered.

As always, Let's commit our code and push the code to GitHub., But before that, remove the string public from .gitignore so we can push our public directory to GitHub as well.

You can checkout the Repository here https://github.com/dhruv-kumar-jha/react-static-complete-website/tree/V4.0

Step#5: Let's Publish Our Website

It's time to re-publish our website.

You will have to tell your hosting provider to use the HTML file for the given route and if the html file is not present then use the 200.html file.

This will be different for different providers, The settings for Firebase is below, If you run into any issue with your static host provider, Let me know., It should be very straightforward though.

Edit the file firebase.json and replace its content with

{ "hosting": { "public": "public", "rewrites": [ { "source": "**", "destination": "/200.html" } ], "cleanUrls": true, "trailingSlash": false } }

Let's commit and push our code to GitHub (again).

If you've hosted your website on Firebase as well, Just run the command yarn deploy and it will re-publish our website.

You can checkout the website here https://react-static-website.firebaseapp.com/

Luckily Firebase will automatically use the gzip version of our files., You might have to check how to use gzip version with your hosting provider.

So What Did We Achieve?

Although the website looks same (except for the slider which I will get back to).

The major differennce here is now We're pre-rendering all of our pages. This means all the Search engines and Crawlers can see our content and index our website.

This is a huge Win.

In next tutorial we will add blog section and add Tags for Social Sharing and Google Snippets.

Thank your for your time.