├── .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 | 
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 | });
--------------------------------------------------------------------------------