Transforming Html with Webpack
by John Vincent
Posted on May 25, 2018
How to Transform Html with Webpack
Html often contains data that should be injected from the environment.
The following describes a solution.
General
React/Redux/Node/Express Ecosystem
Specifically, I needed this for React applications. The index.html
file is static and needs to be for SEO but is basically the same for all applications except the application specific data.
Also, in practice, the data for the index.html
file will become known as the application development progresses.
Further, this data is also often different for Development and Production environments. Multiple versions of Html files is trouble waiting to happen, so let's resolve this.
Requirements
- Html as a template
- Data in template can be replaced
- Replacement data should be stored in the environment
- Transformed Html should be copied to destination.
Template
This mechanism can be used for any file. For this example, I only use index.ejs
, which will be transformed to index.html
templates/index.ejs
<!DOCTYPE html>
<html>
<head>
<title>{{data.TITLE}}</title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="{{ data.DESCRIPTION }}">
<meta name="keywords" content="{{ data.KEYWORDS }}">
<meta name="google-site-verification" content="{{ data.GOOGLE_SITE_VERIFICATION }}" />
<meta name="author" content="{{ data.AUTHOR }}">
<link rel="author" href="{{ data.GOOGLE_PROFILE }}" />
<meta property="fb:app_id" content="{{ data.FACEBOOK_APP_ID }}">
<meta property="og:locale" content="en_US" />
<meta property="og:type" content="website" />
<meta property="og:title" content="{{ data.TITLE }}" />
<meta property="og:description" content="{{ data.DESCRIPTION}}" />
<meta property="og:url" content="{{ data.HOME_URL }}">
<meta property="og:image" content="{{ data.HOME_URL }}/{{ data.AUTHOR_IMAGE }}">
<meta property="og:image:alt" content="{{ data.AUTHOR }}">
<meta property="og:image:width" content="449" />
<meta property="og:image:height" content="449" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="{{ data.TITLE }}" />
<meta name="twitter:description" content="{{ data.DESCRIPTION }}" />
<meta name="twitter:site" content="{{ data.TWITTER_USERNAME }}" />
<meta name="twitter:image" content="{{ data.HOME_URL }}/{{ data.AUTHOR_IMAGE }}" />
<meta name="twitter:creator " content="{{ data.TWITTER_USERNAME }}" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#2196f3">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="theme-color" content="#ffffff">
<!-- Google authentication -->
<script src="https://apis.google.com/js/platform.js?onload=onLoadCallback" async defer></script>
<script>
var gapiPromise = (function () {
return new Promise(function (resolve, reject) {
window.onLoadCallback = function () {
resolve();
};
});
}());
window.app = window.app || {};
window.app.gapiPromise = gapiPromise;
</script>
<!-- Google Analytics -->
<script>
(function (i, s, o, g, r, a, m) {
i['GoogleAnalyticsObject'] = r; i[r] = i[r] || function () {
(i[r].q = i[r].q || []).push(arguments)
}, i[r].l = 1 * new Date(); a = s.createElement(o),
m = s.getElementsByTagName(o)[0]; a.async = 1; a.src = g; m.parentNode.insertBefore(a, m)
})(window, document, 'script', '{{ data.GOOGLE_ANALYTICS_URL }}', 'ga');
ga('create', '{{ data.GOOGLE_ANALYTICS_UA }}', 'auto');
ga('send', 'pageview');
</script>
<!-- End Google Analytics -->
<!-- Fonts -->
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700" rel="stylesheet">
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<!-- Css -->
<link rel="stylesheet" href="./main.bundle.css">
</head>
<body>
<div id="root"></div>
<script src="./bundle.js"></script>
</body>
</html>
Notice that substitution variables are prefixed with 'data'
Environment
Data is placed in .env
Webpack Configuration
Snippet of webpack.config.js
...
const CopyWebpackPlugin = require('copy-webpack-plugin');
const transformTemplate = require('./transformTemplate');
const transforms = require('./transforms');
...
plugins: [
// transform template to index.html with env variables
new CopyWebpackPlugin(
[
{
from: './templates/index.hbs',
to: './index.html',
transform(content, pathname) {
return transformTemplate(content, pathname, transforms);
}
}
],
{ debug: 'info' }
),
...
The key here is to use the transform function of CopyWebpackPlugin
The content is transformed by transformTemplate
transformTemplate.js
//
/* eslint-disable import/no-extraneous-dependencies */
const _ = require('underscore');
function transformTemplate(content, path2, data) {
const layout = content.toString('utf8'); // convert Buffer to string
// prefer handlebars substitution
_.templateSettings = {
interpolate: /\{\{(.+?)\}\}/g
};
// prefer to use a prefix
const compiled = _.template(layout, { variable: 'data' });
const str = compiled(data); // apply the variables
return Buffer.from(str); // convert string to Buffer
}
module.exports = transformTemplate;
which requires transforms as a parameter.
transforms.js
require('dotenv').config(); // read .env
function getEnv(name) {
return process.env[name];
}
const transforms = {
HOME_URL: getEnv('HOME_URL'),
TITLE: getEnv('TITLE'),
DESCRIPTION: getEnv('DESCRIPTION'),
KEYWORDS: getEnv('KEYWORDS'),
AUTHOR: getEnv('AUTHOR'),
AUTHOR_IMAGE: getEnv('AUTHOR_IMAGE'),
TWITTER_USERNAME: getEnv('TWITTER_USERNAME'),
GOOGLE_PROFILE: getEnv('GOOGLE_PROFILE'),
GOOGLE_SITE_VERIFICATION: getEnv('GOOGLE_SITE_VERIFICATION'),
GOOGLE_APP_ID: getEnv('GOOGLE_APP_ID'),
GOOGLE_ANALYTICS_UA: getEnv('GOOGLE_ANALYTICS_UA'),
GOOGLE_ANALYTICS_URL: getEnv('GOOGLE_ANALYTICS_URL'),
FACEBOOK_APP_ID: getEnv('FACEBOOK_APP_ID')
};
module.exports = transforms;
This technique allows for the transformation of any number of files.
React
- Basic React
- Basic React Patterns
- Basic React Redux
- Basic React Redux App
- Basic React Testing with Jest and Enzyme
- Building and deploying MyTunes to johnvincent.io
- Building and deploying React Github Helper App to johnvincent.io
- Deploy React App to Heroku using Travis Continuous Integration
- Deploy TaskMuncher React App to AWS
- First time deploy TaskMuncher React App to Digital Ocean
- Gatsby and Client Only Components
- Gatsby Getting Started
- Gatsby React Icons
- Mac Visual Studio Code
- Material-UI
- Material-UI Pickers
- Material-UI Styling
- Optimizing TaskMuncher with Webpack 4
- Overview of React Gomoku
- Overview of React Hangman
- Overview of React Lights Out
- Overview of React Yahtzee
- React Material-UI
- React Production Issues
- React PropTypes
- React/Redux Node/Express Ecosystem
- Redux Dev Tools
- Responsive Material-UI
- Styling Material-UI components using Styled-Components
- TaskMuncher Performance
- Transforming Html with Webpack
- Update TaskMuncher for Lighthouse Findings
- Update TaskMuncher to be a Progressive Web App
- Update TaskMuncher to use React BrowserRouter
- Update TaskMuncher to Webpack v4, Babel v7, Material-UI v3
- Upgrading Babel and ESLint to use React Advanced Language Features
- Webpack Bundle Analyzer