├── .eslintrc.js ├── .gitignore ├── README.md ├── config └── files.js ├── package-lock.json ├── package.json ├── src ├── components │ ├── Menu.css │ ├── Menu.js │ └── OpenSans-Regular.woff └── pages │ ├── contact.html │ ├── contact.js │ ├── index.html │ ├── index.js │ └── products │ ├── product-1.css │ ├── product-1.html │ ├── product-1.js │ └── product-logo.png └── webpack.config.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | }, 6 | extends: 'airbnb', 7 | globals: { 8 | Atomics: 'readonly', 9 | SharedArrayBuffer: 'readonly', 10 | }, 11 | parserOptions: { 12 | ecmaFeatures: { 13 | jsx: true, 14 | }, 15 | ecmaVersion: 2018, 16 | sourceType: 'module', 17 | }, 18 | plugins: [ 19 | 'react', 20 | ], 21 | rules: { 22 | 'react/prefer-stateless-function': 0, 23 | 'react/jsx-filename-extension': 0 24 | }, 25 | settings: { 26 | 'import/resolver': { 27 | alias: { 28 | map: [ 29 | ['components', './src/components'] 30 | ] 31 | } 32 | } 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | .lock.json 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Multi Page App with React 2 | 3 | A lightweight, flexible Webpack setup with React for multi page application development.
4 | 5 | You should consider this setup when you want to use React for some sections of your page(s), but you don't want to make it Single Page Application (SPA) with only a `
` in the body tag. 6 | 7 | Here's a code sample to illustrate this concept for an example static page with several React components: 8 | ![picture alt](http://assets.miwu.pl/mpa-with-react-example.png "MPA with React example") 9 | 10 | A basic write up of this setup can be found on this [Medium post](https://itnext.io/building-multi-page-application-with-react-f5a338489694). 11 | 12 | **Notice:** the latest code base includes the following updates: 13 | - webpack bundles with hashed filenames 14 | - code linter (with Airbnb style guide) 15 | - enabled CSS modules (added example css styles) 16 | - minimize webpack bundles with TerserPlugin (i.e. js code minification) 17 | - file loader to resolve imports on fonts and images (added example product image) 18 | 19 | 20 | ## Quick Start 21 | 22 | install dependencies 23 | ``` 24 | $ npm install 25 | ``` 26 | 27 | ## Development & Build 28 | 29 | **dev** 30 | 31 | ``` 32 | $ npm start 33 | ``` 34 | ***start** script runs server in development mode with hot module replacement and open the browser after server had been started.* 35 | 36 | **build** 37 | 38 | ``` 39 | $ npm run build 40 | ``` 41 | 42 | ***build** script runs in production mode to improve load time (i.e. minified bundles, lighter weight source maps etc)* 43 | 44 | **linting** 45 | 46 | ``` 47 | $ npm run lint 48 | ``` 49 | 50 | ***lint** script runs linter to check for lint errors in src directory* -------------------------------------------------------------------------------- /config/files.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | 4 | function getFilesFromDir(dir, fileTypes) { 5 | const filesToReturn = []; 6 | function walkDir(currentPath) { 7 | const files = fs.readdirSync(currentPath); 8 | for (let i in files) { 9 | const curFile = path.join(currentPath, files[i]); 10 | if (fs.statSync(curFile).isFile() && fileTypes.indexOf(path.extname(curFile)) != -1) { 11 | filesToReturn.push(curFile); 12 | } else if (fs.statSync(curFile).isDirectory()) { 13 | walkDir(curFile); 14 | } 15 | } 16 | }; 17 | walkDir(dir); 18 | return filesToReturn; 19 | } 20 | 21 | module.exports = getFilesFromDir; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "multi-page-app-with-react", 3 | "version": "1.0.0", 4 | "description": "Multi Page Application with React", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "eslint --ext .jsx,.js src", 8 | "build": "webpack --mode production", 9 | "start": "webpack-dev-server --mode development --hot --open --port 3100" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/przemek-nowicki/multi-page-app-with-react.git" 14 | }, 15 | "keywords": [ 16 | "react mpa", 17 | "webpack4 multi-pages", 18 | "react multi-page-app", 19 | "react multi page application" 20 | ], 21 | "author": "Przemek Nowicki", 22 | "license": "ISC", 23 | "bugs": { 24 | "url": "https://github.com/przemek-nowicki/multi-page-app-with-react/issues" 25 | }, 26 | "homepage": "https://github.com/przemek-nowicki/multi-page-app-with-react#readme", 27 | "devDependencies": { 28 | "@babel/core": "^7.6.0", 29 | "@babel/preset-env": "^7.6.0", 30 | "@babel/preset-react": "^7.0.0", 31 | "babel-loader": "^8.0.6", 32 | "css-loader": "^2.1.1", 33 | "eslint": "^5.16.0", 34 | "eslint-config-airbnb": "^17.1.1", 35 | "eslint-plugin-import": "^2.18.2", 36 | "eslint-plugin-jsx-a11y": "^6.2.3", 37 | "eslint-plugin-react": "^7.14.3", 38 | "file-loader": "^3.0.1", 39 | "html-webpack-plugin": "^3.2.0", 40 | "style-loader": "^0.23.1", 41 | "uglifyjs-webpack-plugin": "^2.2.0", 42 | "webpack": "^4.39.3", 43 | "webpack-cli": "^3.3.8", 44 | "webpack-dev-server": "^3.8.0" 45 | }, 46 | "dependencies": { 47 | "eslint-import-resolver-alias": "^1.1.2", 48 | "react": "^16.9.0", 49 | "react-dom": "^16.9.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/components/Menu.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'MenuFont'; 3 | src: url('./OpenSans-Regular.woff') format('woff'); 4 | } 5 | 6 | .Menu { 7 | color: #808080; 8 | font-family: 'MenuFont'; 9 | list-style-type: square; 10 | } -------------------------------------------------------------------------------- /src/components/Menu.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import style from './Menu.css'; 3 | 4 | export default class Menu extends Component { 5 | render() { 6 | return ( 7 | 12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/components/OpenSans-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/przemek-nowicki/multi-page-app-with-react/7c4ec1a0b72036f1f712e404b3645365e4dc2acb/src/components/OpenSans-Regular.woff -------------------------------------------------------------------------------- /src/pages/contact.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Contact Page 6 | 7 | 8 | 9 |

Lorem ipsum dolor sit amet consectetur adipisicing elit.

10 | 11 | -------------------------------------------------------------------------------- /src/pages/contact.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Menu from 'components/Menu'; 4 | 5 | ReactDOM.render(, document.getElementById('menu')); 6 | -------------------------------------------------------------------------------- /src/pages/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Home Page 6 | 7 | 8 | 9 |

Lorem ipsum dolor sit amet consectetur adipisicing elit.

10 | 11 | -------------------------------------------------------------------------------- /src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Menu from 'components/Menu'; 4 | 5 | ReactDOM.render(, document.getElementById('menu')); 6 | -------------------------------------------------------------------------------- /src/pages/products/product-1.css: -------------------------------------------------------------------------------- 1 | p { 2 | color: red; 3 | font-weight: bold; 4 | } -------------------------------------------------------------------------------- /src/pages/products/product-1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Product 1 Page 6 | 7 | 8 | 9 |

Lorem ipsum dolor sit amet consectetur adipisicing elit.

10 | Picture of the product 11 | 12 | -------------------------------------------------------------------------------- /src/pages/products/product-1.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Menu from 'components/Menu'; 4 | 5 | import './product-1.css'; 6 | import productPicture from './product-logo.png'; 7 | 8 | ReactDOM.render(, document.getElementById('menu')); 9 | document.getElementById('product-pic').setAttribute('src', productPicture); 10 | -------------------------------------------------------------------------------- /src/pages/products/product-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/przemek-nowicki/multi-page-app-with-react/7c4ec1a0b72036f1f712e404b3645365e4dc2acb/src/pages/products/product-logo.png -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const HtmlWebPackPlugin = require("html-webpack-plugin"); 3 | const getFilesFromDir = require("./config/files"); 4 | const PAGE_DIR = path.join("src", "pages", path.sep); 5 | 6 | const htmlPlugins = getFilesFromDir(PAGE_DIR, [".html"]).map( filePath => { 7 | const fileName = filePath.replace(PAGE_DIR, ""); 8 | // { chunks:["contact", "vendor"], template: "src/pages/contact.html", filename: "contact.html"} 9 | return new HtmlWebPackPlugin({ 10 | chunks:[fileName.replace(path.extname(fileName), ""), "vendor"], 11 | template: filePath, 12 | filename: fileName 13 | }) 14 | }); 15 | 16 | // { contact: "./src/pages/contact.js" } 17 | const entry = getFilesFromDir(PAGE_DIR, [".js"]).reduce( (obj, filePath) => { 18 | const entryChunkName = filePath.replace(path.extname(filePath), "").replace(PAGE_DIR, ""); 19 | obj[entryChunkName] = `./${filePath}`; 20 | return obj; 21 | }, {}); 22 | 23 | module.exports = (env, argv) => ({ 24 | entry: entry, 25 | output: { 26 | path: path.join(__dirname, "build"), 27 | filename: "[name].[hash:4].js" 28 | }, 29 | devtool: argv.mode === 'production' ? false : 'eval-source-maps', 30 | plugins: [ 31 | ...htmlPlugins 32 | ], 33 | resolve:{ 34 | alias:{ 35 | src: path.resolve(__dirname, "src"), 36 | components: path.resolve(__dirname, "src", "components") 37 | } 38 | }, 39 | module: { 40 | rules: [ 41 | { 42 | test: /\.js$/, 43 | exclude: /node_modules/, 44 | use: { 45 | loader:"babel-loader", 46 | options:{ 47 | presets: [ 48 | "@babel/preset-env", 49 | "@babel/preset-react" 50 | ], 51 | } 52 | }, 53 | }, 54 | { 55 | test: /\.css$/, 56 | use: ["style-loader", {loader: "css-loader", options: {modules: true}}], 57 | exclude: /node_modules/, 58 | }, 59 | { 60 | test: /\.(svg|jpg|gif|png)$/, 61 | use: [ 62 | { 63 | loader: 'file-loader', 64 | options: { 65 | name: '[name].[ext]', 66 | outputPath: (url, resourcePath, context) => { 67 | if(argv.mode === 'development') { 68 | const relativePath = path.relative(context, resourcePath); 69 | return `/${relativePath}`; 70 | } 71 | return `/assets/images/${path.basename(resourcePath)}`; 72 | } 73 | } 74 | } 75 | ] 76 | }, 77 | { 78 | test: /\.(woff|woff2|eot|ttf|otf)$/, 79 | use: [ 80 | { 81 | loader: 'file-loader', 82 | options: { 83 | outputPath: (url, resourcePath, context) => { 84 | if(argv.mode === 'development') { 85 | const relativePath = path.relative(context, resourcePath); 86 | return `/${relativePath}`; 87 | } 88 | return `/assets/fonts/${path.basename(resourcePath)}`; 89 | } 90 | } 91 | } 92 | ] 93 | }] 94 | }, 95 | optimization: { 96 | minimize: argv.mode === 'production' ? true : false, 97 | splitChunks: { 98 | cacheGroups: { 99 | vendor: { 100 | test: /node_modules/, 101 | chunks: "initial", 102 | name: "vendor", 103 | enforce: true 104 | } 105 | } 106 | } 107 | } 108 | }); --------------------------------------------------------------------------------