├── .babelrc ├── .gitignore ├── README.md ├── dist ├── about │ └── index.html ├── index.html ├── index.js └── repos │ └── index.html ├── index.js ├── modules ├── About.js ├── App.js ├── Home.js ├── NavLink.js ├── Repo.js ├── Repos.js └── routes.js ├── package.json ├── server.js ├── template.js ├── webpack.config.js └── webpack.server.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "react", 5 | "stage-0" 6 | ], 7 | "plugins": [] 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | server.bundle.js 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This repo is a demo of [static-site-generator-webpack-plugin](https://github.com/markdalgleish/static-site-generator-webpack-plugin), showing you how to generate a static site with React, React-Router, and Webpack. 2 | 3 | It is inspired by Jxnblk's article ["Static Site Generation With React Aad Webpack"](http://jxnblk.com/writing/posts/static-site-generation-with-react-and-webpack/). 4 | 5 | ## Usage 6 | 7 | First, clone the repo. 8 | 9 | ```bash 10 | $ git clone https://github.com/ruanyf/webpack-static-site-demo.git 11 | ``` 12 | 13 | Second, install the dependencies. 14 | 15 | ```bash 16 | $ cd webpack-static-site-demo 17 | $ npm intall 18 | ``` 19 | 20 | Third, generate the static files. 21 | 22 | ```bash 23 | $ npm run build 24 | ``` 25 | 26 | Now, you should see the static files under the `dist` subdirectory. 27 | 28 | Finally, run the server. 29 | 30 | ```bash 31 | $ npm start 32 | ``` 33 | 34 | Visit http://localhost:8080 . You will find it serves a static site, and at the same time has the experience of single page App. 35 | 36 | ## Explanation 37 | 38 | ### Webpack config 39 | 40 | The `static-site-generator-webpack-plugin` add a handler function into webpack's configure. 41 | 42 | ```javascript 43 | var StaticSiteGeneratorPlugin = require('static-site-generator-webpack-plugin'); 44 | 45 | module.exports = { 46 | // ... 47 | plugins: [ 48 | new StaticSiteGeneratorPlugin('main', paths, {template: template, bundlejs: 'bundle.js'}) 49 | ] 50 | }; 51 | ``` 52 | 53 | Note you have to compile the Webpack's output module into the format of UMD or CommonJS. 54 | 55 | ```javascript 56 | output: { 57 | filename: 'index.js', 58 | path: 'dist', 59 | /* IMPORTANT! 60 | * You must compile to UMD or CommonJS 61 | * so it can be required in a Node context: */ 62 | libraryTarget: 'umd' 63 | }, 64 | ``` 65 | 66 | ### Constructor's Arguments 67 | 68 | `static-site-generator-webpack-plugin`'s constructor accepts three arguments. 69 | 70 | ```javascript 71 | function StaticSiteGeneratorWebpackPlugin(renderSrc, outputPaths, locals) { 72 | this.renderSrc = renderSrc; 73 | this.outputPaths = Array.isArray(outputPaths) ? outputPaths : [outputPaths]; 74 | this.locals = locals; 75 | } 76 | ``` 77 | 78 | (1) `renderSrc` 79 | 80 | `renderSrc` is asset file's name or chunk name. For example, `webpack.config.js` looks like the following. 81 | 82 | ```javascript 83 | // webpack.config.js 84 | entry: { 85 | main: './index.js' 86 | }, 87 | output: { 88 | path: 'public', 89 | filename: 'bundle.js', 90 | libraryTarget: 'umd' 91 | } 92 | ``` 93 | 94 | Then the `renderSrc` could be `main` or `bundle.js`. 95 | 96 | (2) `outputPaths` 97 | 98 | `outputPaths` is an array which comprises the static site's paths. 99 | 100 | ```javascript 101 | var paths = [ 102 | '/', 103 | '/app/', 104 | '/inbox/', 105 | '/calendar/' 106 | ]; 107 | ``` 108 | 109 | if your `outputPaths` is the above, output will be the following. 110 | 111 | - /index.html 112 | - /app/index.html 113 | - /inbox/index.html 114 | - /calendar/index.html 115 | 116 | If the providing paths end in `.html`, you can generate custom file names other than the default `index.html`. 117 | 118 | ```javascript 119 | var paths = [ 120 | '/a.html', 121 | '/app/b.html', 122 | '/inbox/c.html', 123 | '/calendar/d.html' 124 | ]; 125 | ``` 126 | 127 | (3) locals 128 | 129 | `locals` is an object which you put every extra property into. 130 | 131 | ```javascript 132 | plugins: [ 133 | new StaticSiteGeneratorPlugin('index.js', paths, { template: template }), 134 | ] 135 | ``` 136 | 137 | In the above code, a `template` property could be get from `locals`. 138 | 139 | You also can get three default properties from `locals`. 140 | 141 | - `locals.path`: The path currently being rendered 142 | - `locals.assets`: An object containing all assets 143 | - `locals.webpackStats`: Webpack's stats object 144 | 145 | ### Entry file 146 | 147 | The entry JavaScript file looks like the following. 148 | 149 | ```javascript 150 | // Client render (optional): 151 | if (typeof document !== 'undefined') { 152 | // Client render code goes here... 153 | } 154 | 155 | // Exported static site renderer: 156 | module.exports = function render(locals, callback) { 157 | callback(null, locals.template({ html: '

' + locals.path + '

' })); 158 | }; 159 | ``` 160 | 161 | The entry file should be a CommonJS module which exports a function to generate the static html file. 162 | 163 | The exported function receives two arguments: 164 | 165 | - `locals` object. We usually put the template into `locals.template`. 166 | - `callback` function. It receives two arguments: `err` object and html string provided by `static-site-generator-webpack-plugin`. If no error, `callback` transfers the html string to Webpack to write down on hard disks later. 167 | 168 | ## License 169 | 170 | MIT 171 | 172 | -------------------------------------------------------------------------------- /dist/about/index.html: -------------------------------------------------------------------------------- 1 | My First React Router App

React Router Tutorial

About
-------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | My First React Router App

React Router Tutorial

Home
-------------------------------------------------------------------------------- /dist/repos/index.html: -------------------------------------------------------------------------------- 1 | My First React Router App

React Router Tutorial

Repos

-------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { renderToStaticMarkup } from 'react-dom/server' 4 | import { Router, browserHistory, createMemoryHistory, RouterContext, match } from 'react-router' 5 | import routes from './modules/routes' 6 | 7 | if (typeof document !== 'undefined') { 8 | render( 9 | , 10 | document.getElementById('app') 11 | ) 12 | } 13 | 14 | export default (locals, callback) => { 15 | const history = createMemoryHistory(); 16 | const location = history.createLocation(locals.path); 17 | 18 | match({ routes, location }, (error, redirectLocation, renderProps) => { 19 | callback(null, locals.template({ 20 | html: renderToStaticMarkup(), 21 | assets: locals.assets 22 | })); 23 | }); 24 | }; 25 | 26 | -------------------------------------------------------------------------------- /modules/About.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default React.createClass({ 4 | render() { 5 | return
About
6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /modules/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import NavLink from './NavLink' 3 | 4 | export default React.createClass({ 5 | render() { 6 | return ( 7 |
8 |

React Router Tutorial

9 |
    10 |
  • Home
  • 11 |
  • About
  • 12 |
  • Repos
  • 13 |
14 | {this.props.children} 15 |
16 | ) 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /modules/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default React.createClass({ 4 | render() { 5 | return
Home
6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /modules/NavLink.js: -------------------------------------------------------------------------------- 1 | // modules/NavLink.js 2 | import React from 'react' 3 | import { Link } from 'react-router' 4 | 5 | export default React.createClass({ 6 | render() { 7 | return 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /modules/Repo.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default React.createClass({ 4 | render() { 5 | const { userName, repoName } = this.props.params 6 | return ( 7 |
8 |

{userName} / {repoName}

9 |
10 | ) 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /modules/Repos.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import NavLink from './NavLink' 3 | 4 | export default React.createClass({ 5 | contextTypes: { 6 | router: React.PropTypes.object 7 | }, 8 | 9 | handleSubmit(event) { 10 | event.preventDefault() 11 | const userName = event.target.elements[0].value 12 | const repo = event.target.elements[1].value 13 | const path = `/repos/${userName}/${repo}` 14 | this.context.router.push(path) 15 | }, 16 | 17 | render() { 18 | return ( 19 |
20 |

Repos

21 |
    22 |
  • React Router
  • 23 |
  • React
  • 24 |
  • 25 |
    26 | / {' '} 27 | {' '} 28 | 29 |
    30 |
  • 31 |
32 | {this.props.children} 33 |
34 | ) 35 | } 36 | }) 37 | -------------------------------------------------------------------------------- /modules/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Router, Route, browserHistory, IndexRoute } from 'react-router' 3 | import App from './App' 4 | import About from './About' 5 | import Repos from './Repos' 6 | import Repo from './Repo' 7 | import Home from './Home' 8 | 9 | module.exports = ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ) 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-static-site-demo", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "if-env NODE_ENV=production && npm run start:prod || npm run start:dev", 8 | "start:dev": "webpack-dev-server --content-base dist/ --history-api-fallback", 9 | "start:prod": "npm run build && node server.bundle.js", 10 | "build:client": "webpack", 11 | "build:server": "webpack --config webpack.server.config.js", 12 | "build": "npm run build:client && npm run build:server" 13 | }, 14 | "author": "", 15 | "license": "MIT", 16 | "dependencies": { 17 | "compression": "^1.6.1", 18 | "express": "^4.13.4", 19 | "if-env": "^1.0.0", 20 | "react": "^0.14.7", 21 | "react-dom": "^0.14.7", 22 | "react-router": "2.x" 23 | }, 24 | "devDependencies": { 25 | "babel-core": "^6.5.1", 26 | "babel-loader": "^6.2.2", 27 | "babel-preset-es2015": "^6.5.0", 28 | "babel-preset-react": "^6.5.0", 29 | "ejs": "^2.4.1", 30 | "http-server": "^0.8.5", 31 | "static-site-generator-webpack-plugin": "^2.0.1", 32 | "webpack": "^1.12.13", 33 | "webpack-dev-server": "^1.14.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import path from 'path' 3 | import compression from 'compression' 4 | import React from 'react' 5 | import { renderToString } from 'react-dom/server' 6 | import { match, RouterContext } from 'react-router' 7 | import routes from './modules/routes' 8 | 9 | var app = express() 10 | 11 | app.use(compression()) 12 | 13 | // serve our static stuff like index.css 14 | app.use(express.static(path.join(__dirname, 'public'), {index: false})) 15 | 16 | // send all requests to index.html so browserHistory works 17 | app.get('*', (req, res) => { 18 | match({ routes, location: req.url }, (err, redirect, props) => { 19 | if (err) { 20 | res.status(500).send(err.message) 21 | } else if (redirect) { 22 | res.redirect(redirect.pathname + redirect.search) 23 | } else if (props) { 24 | // hey we made it! 25 | const appHtml = renderToString() 26 | res.send(renderPage(appHtml)) 27 | } else { 28 | res.status(404).send('Not Found') 29 | } 30 | }) 31 | }) 32 | 33 | function renderPage(appHtml) { 34 | return ` 35 | 36 | 37 | 38 | My First React Router App 39 | 40 |
${appHtml}
41 | 42 | ` 43 | } 44 | 45 | var PORT = process.env.PORT || 8080 46 | app.listen(PORT, function() { 47 | console.log('Production Express server running at localhost:' + PORT) 48 | }) 49 | -------------------------------------------------------------------------------- /template.js: -------------------------------------------------------------------------------- 1 | module.exports = function renderPage(props) { 2 | return ( 3 | '' 4 | + '' 5 | + '' 6 | + 'My First React Router App' 7 | + '
' + props.html + '
' 8 | + '' 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var webpack = require('webpack') 3 | var StaticSiteGeneratorPlugin = require('static-site-generator-webpack-plugin'); 4 | var template = require('./template'); 5 | 6 | var paths = [ 7 | '/', 8 | '/repos', 9 | '/about' 10 | ]; 11 | 12 | module.exports = { 13 | entry: { 14 | 'main': './index.js', 15 | }, 16 | 17 | output: { 18 | path: 'dist', 19 | filename: 'index.js', 20 | libraryTarget: 'umd' 21 | }, 22 | 23 | plugins: process.env.NODE_ENV === 'production' ? [ 24 | new webpack.optimize.DedupePlugin(), 25 | new webpack.optimize.OccurrenceOrderPlugin(), 26 | new webpack.optimize.UglifyJsPlugin() 27 | ] : [ 28 | new StaticSiteGeneratorPlugin('main', paths, { template: template, assets: 'index.js' }) 29 | ], 30 | 31 | module: { 32 | loaders: [ 33 | { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader?presets[]=es2015&presets[]=react' } 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /webpack.server.config.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var path = require('path') 3 | 4 | module.exports = { 5 | 6 | entry: path.resolve(__dirname, 'server.js'), 7 | 8 | output: { 9 | filename: 'server.bundle.js' 10 | }, 11 | 12 | target: 'node', 13 | 14 | // keep node_module paths out of the bundle 15 | externals: fs.readdirSync(path.resolve(__dirname, 'node_modules')).concat([ 16 | 'react-dom/server' 17 | ]).reduce(function (ext, mod) { 18 | ext[mod] = 'commonjs ' + mod 19 | return ext 20 | }, {}), 21 | 22 | node: { 23 | __filename: false, 24 | __dirname: false 25 | }, 26 | 27 | module: { 28 | loaders: [ 29 | { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader?presets[]=es2015&presets[]=react' } 30 | ] 31 | } 32 | 33 | } 34 | --------------------------------------------------------------------------------