You can checkout Part #1 of this tutorial here Building a Static Website Using React JS: Part #1 Project Setup and Website UI
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.
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.
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.
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
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
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.
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
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.
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.