I'm having trouble figuring out how to have multiple pages in a Vue CLI project. Right now I have my home page with a few components and I want to create another page but I do not know how to do that. Am I supposed to create multiple html files where the index.html by default is? In a simple file structure with css js img folder and html files as pages I know that creating another html file means making another page. But I don't understand how this works with Vue CLI project.

I saw stuff like vue-router and "pages" in Vue documentation but I do not understand them very well. What are my alternatives? Is there a guide that explains that in detail, because I wasn't able to find any, let alone detailed. Would be very happy if you could help! Thank you!


First: always read the official documentation. With Vue you can build a SPA, and a MPA is also no problem. Just follow the guides:

  • https://cli.vuejs.org/
  • https://cli.vuejs.org/guide/html-and-static-assets.html#building-a-multi-page-app
  • https://cli.vuejs.org/config/#pages

You should create a new project with Vue CLI 3. Once you've created your project set it to be manually configured. Make sure you don't choose the SPA option. Vue will then create a nice "start" project using a MPA approach. After that, just repeat the config on vue.config.js.


Updated #1

It seems that some updates on Vue Cli, have changed the way to build a MPA app, so:

  • Create a new application vue create test
  • Choose Manual configuration

The boilerplate created will be for a SPA. So make the following changes:

  • Create a folder under src named pages (optional)

  • Into this folder create your own pages: Home, About, etc.

  • Copy and paste the App.vue and main.js from src, into your new folders - Home, etc.

  • Format the App.vue into this folders, to your liking.

  • Create a vue.config.js and set it like this: https://cli.vuejs.org/config/#pages

Below, I have three images demonstrating this:

  • First: a fresh new app
  • Second: this same app, with the changes I made above
  • Third: the vue.config.js from this app

fresh new app this same app, with the changes I made above the vue.config.js from this app

You don't need to create the pages folder, this is just to get the idea.

Link to GitHub: Building a MPA App


EDIT: Vue has this built-in. Skip to the bottom for more.

Original answer:

There are two ways to interpret your question, and therefore to answer it.

The first interpretation is: "how can I support routing to different pages within the same single-page app, e.g. localhost:8080/about and localhost:8080/report etc?". The answer to this is to use the router. It's reasonably straightforward and works well.

The second interpretation is: "my app is complex, and I have multiple single-page applications, e.g. one app for the 'website' part, one app for consumers to log in and do work, one app for admins, etc - how can vue do this, without making three entirely separate repositories?"

The answer to the latter is a single repository with multiple single-page apps. This demo looks like exactly what you're after:

https://github.com/Plortinus/vue-multiple-pages/

Look in particular at: https://github.com/Plortinus/vue-multiple-pages/blob/master/vue.config.js

Updated answer:

It turns out that vuejs has the idea of multiple top-level pages built-in. I mean, it makes sense - it's going to be really common, despite what many incorrect answers are saying about "no, it's for single page apps"!

You want the pages option in the vue.config.js file:

https://cli.vuejs.org/config/#pages

If your project doesn't have that file in the root directory, create it and vuejs will discover it.

There is a long and a short way to define each page. I used the short form here:

module.exports = {
  pages: {
    index: 'src/pages/index/main.ts',
    experiment: 'src/pages/experiment/main.ts'
  }
}

You don't have to put your work under "pages". It could be "/src/apps/index/index.ts" or whatever.

After moving code around and changing some imports from:

import HelloWorld from './components/HelloWorld'

to

import HelloWorld from '@/components/HelloWorld'

The app works - but the "experiment" app in my repo had to be loaded like this:

http://localhost:8080/experiment.html

Pretty ugly, and even worse because it uses the router which resulted in URLs like:

http://localhost:8080/experiment.html/about

Ugh.

Fortunately, this stackoverflow answer solved it. Update the vue.config.js file to include devServer options (make sure this is at the top level of the exported object:

devServer: {
  historyApiFallback: {
    rewrites: [
      { from: /\/index/, to: '/index.html' },
      { from: /\/experiment/, to: '/experiment.html' }
    ]
  }
}

Then also modify the router.ts file to append the extra path (in my case "experiment/":

export default new Router({
  mode: 'history',
  base: process.env.BASE_URL + 'experiment/',
  ...

Then URLs resolve nicely, e.g.: http://localhost:8080/experiment/about


This may not be relevant to the question, but bear with me, maybe my answer can help someone. I use webpack+vue, and I have figured out how to build multiple pages applications. Here my webpack.config.js:

const path = require('path');
const fs = require('fs')
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const TerserPlugin = require('terser-webpack-plugin');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");

module.exports = {
    entry: {
        app: './src/app.js',
        mgmt: ['./src/modules/mgmt/mgmt.js'],
        login: './src/modules/login/login.js'
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        // publicPath: '/ahezime/',
        filename: (chunkData) => {
            console.log('chuckData.chunk.name => ', chunkData.chunk.name)
            return chunkData.chunk.name === 'app' ? './[name].bundle.js' : './[name]/[name].bundle.js';
        }
    },
    optimization: {
        minimizer: [
            new TerserPlugin(),
            new OptimizeCSSAssetsPlugin({})
        ]
    },
    plugins: [
        new MiniCssExtractPlugin({
            filename: "[name].css",
            chunkFilename: "[id].css"
        }),
        new CleanWebpackPlugin(['dist']),
        new VueLoaderPlugin(),
        new HtmlWebpackPlugin({
            title: 'app',
            template: './src/app.html',
            // inject: false,
            chunks: ['app'],
            filename: './index.html'
        }),
        new HtmlWebpackPlugin({
            title: 'mgmt',
            template: './src/modules/mgmt/mgmt.html',
            // inject: false,
            chunks: ['mgmt'],
            filename: './mgmt/index.html'
        }),
        new HtmlWebpackPlugin({
            title: 'login',
            template: './src/modules/login/login.html',
            // inject: false,
            chunks: ['login'],
            filename: './login/index.html'
        })
    ],
    module: {
        rules: [
            {
                test: /\.m?js$/,
                exclude: /(node_modules|bower_components)/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env'],
                        plugins: ['@babel/plugin-proposal-object-rest-spread']
                    }
                }
            }
        ],
        rules: [
            {
                test: /\.vue$/,
                exclude: /node_modules/,
                loader: 'vue-loader'
            },
            {
                test: /\.css$/,
                use: [
                    'vue-style-loader',
                    'style-loader',
                    'css-loader',
                    'sass-loader'
                ]
            },
            {
                test: /\.scss?$/,
                use: ['style-loader', 'css-loader', 'sass-loader']
            },
            {
                test: /\.(png|svg|jpg|gif)$/,
                use: [
                    'file-loader'
                ]
            },
            {
                test: /\.(woff|woff2|eot|ttf|otf)$/,
                use: [
                    'file-loader'
                ]
            }
        ]
    }
};

And here's my directory structure:

https://i.stack.imgur.com/uFvKx.png

And you can jump pages:

<template>
    <div>
        <h1>App</h1>
        <div>
            <a href="./login">Please click me, and let take you into the login page!!!</a>
        </div>
        <span>Before computed: {{ message }} </span>
        <br>
        <span>Afer computed: {{ computedMessage() }} </span>
    </div>
</template>

<script>
    export default {
        data() {
            return {
                message: 'Hello World!'
            }
        },
        computed: {
            reversedMessage: function() {
                return this.message.split('').reverse().join('')
            }
        },
        methods: {
            computedMessage: function() {
                return this.message.split('').reverse().join('')
            }
        }
    }
</script>