├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .postcssrc.js ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── babel.config.js ├── bin └── server.js ├── netlify.toml ├── package.json ├── public ├── _redirects ├── favicon.ico └── index.html ├── src ├── components │ ├── App.vue │ ├── app │ │ ├── AppContentBlock.vue │ │ └── AppTeaser.vue │ ├── layouts │ │ └── LayoutDefault.vue │ └── views │ │ ├── About.vue │ │ └── Home.vue ├── main.js ├── models │ ├── content-block.js │ ├── image.js │ ├── landing-page.js │ └── teaser.js ├── router.js ├── store │ ├── action-types.js │ ├── index.js │ ├── modules │ │ └── landing-page.js │ └── mutation-types.js └── utils │ ├── api.js │ ├── inject-initial-state.js │ └── register-store-module.js ├── vue.config.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 2 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | !/.*.js 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | `plugin:vue/recommended`, 5 | `@avalanche/eslint-config`, 6 | ], 7 | rules: { 8 | 'no-console': process.env.NODE_ENV === `production` ? `error` : `warn`, 9 | 'no-debugger': process.env.NODE_ENV === `production` ? `error` : `warn`, 10 | }, 11 | parserOptions: { 12 | parser: `babel-eslint`, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Numerous always-ignore extensions 2 | *.diff 3 | *.err 4 | *.log 5 | *.orig 6 | *.rej 7 | *.swo 8 | *.swp 9 | *.vi 10 | *.zip 11 | *~ 12 | 13 | # OS or Editor folders 14 | ._* 15 | .cache 16 | .DS_Store 17 | .idea 18 | .project 19 | .settings 20 | .tmproj 21 | *.esproj 22 | *.sublime-project 23 | *.sublime-workspace 24 | nbproject 25 | Thumbs.db 26 | 27 | # Folders to ignore 28 | dist 29 | node_modules 30 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | - Using welcoming and inclusive language 12 | - Being respectful of differing viewpoints and experiences 13 | - Gracefully accepting constructive criticism 14 | - Focusing on what is best for the community 15 | - Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | - Trolling, insulting/derogatory comments, and personal or political attacks 21 | - Public or private harassment 22 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | - Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at markus.oberlehner@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Reporting Issues 4 | 5 | Found a problem? Want a new feature? 6 | 7 | - See if your issue or idea has [already been reported]. 8 | - Provide a [reduced test case] or a [live example]. 9 | 10 | Remember, a bug is a *demonstrable problem* caused by *our* code. 11 | 12 | ## Submitting Pull Requests 13 | 14 | Pull requests are the greatest contributions, so be sure they are focused in scope, and do avoid unrelated commits. 15 | 16 | 1. To begin, [fork this project], clone your fork, and add our upstream. 17 | ```bash 18 | # Clone your fork of the repo into the current directory 19 | git clone https://github.com//how-to-pre-render-vue-powered-websites-with-webpack 20 | # Navigate to the newly cloned directory 21 | cd how-to-pre-render-vue-powered-websites-with-webpack 22 | # Assign the original repo to a remote called "upstream" 23 | git remote add upstream https://github.com/maoberlehner/how-to-pre-render-vue-powered-websites-with-webpack 24 | # Install the tools necessary for development 25 | yarn install 26 | ``` 27 | 28 | 2. Create a branch for your feature or hotfix: 29 | ```bash 30 | # Move into a new branch for a feature 31 | git checkout -b feature/thing 32 | ``` 33 | 34 | ```bash 35 | # Move into a new branch for a hotfix 36 | git checkout -b hotfix/something 37 | ``` 38 | 39 | 3. Push your branch up to your fork: 40 | ```bash 41 | # Push a feature branch 42 | git push origin feature/thing 43 | ``` 44 | 45 | ```bash 46 | # Push a hotfix branch 47 | git push origin hotfix/something 48 | ``` 49 | 50 | 4. Now [open a pull request] with a clear title and description. 51 | 52 | [already been reported]: https://github.com/maoberlehner/how-to-pre-render-vue-powered-websites-with-webpack/issues 53 | [fork this project]: https://github.com/maoberlehner/how-to-pre-render-vue-powered-websites-with-webpack/fork 54 | [live example]: http://codepen.io/pen 55 | [open a pull request]: https://help.github.com/articles/using-pull-requests/ 56 | [reduced test case]: https://css-tricks.com/reduced-test-cases/ 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Markus Oberlehner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # How to Pre-render Vue.js Powered Websites With webpack 2 | 3 | [![Patreon](https://img.shields.io/badge/patreon-donate-blue.svg)](https://www.patreon.com/maoberlehner) 4 | [![Donate](https://img.shields.io/badge/Donate-PayPal-blue.svg)](https://paypal.me/maoberlehner) 5 | 6 | This is an example project for the following article: [How to Pre-render Vue.js Powered Websites With webpack](https://markus.oberlehner.net/blog/how-to-pre-render-vue-powered-websites-with-webpack/) 7 | 8 | ## Build Setup 9 | 10 | ```bash 11 | # Install dependencies. 12 | npm install 13 | 14 | # Serve with hot reload. 15 | npm run serve 16 | 17 | # Build for production with minification. 18 | npm run build 19 | 20 | # Serve the production build. 21 | npm run serve-production 22 | ``` 23 | 24 | ## Instant deploy to Netlify 25 | 26 | By clicking this button you can automatically clone this repository, setup a new site in [Netlify](https://www.netlify.com) and run the build and deployment to the Netlify CDN. 27 | 28 | [![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/maoberlehner/how-to-pre-render-vue-powered-websites-with-webpack) 29 | 30 | ## About 31 | 32 | ### Author 33 | 34 | Markus Oberlehner 35 | Website: https://markus.oberlehner.net 36 | Twitter: https://twitter.com/MaOberlehner 37 | PayPal.me: https://paypal.me/maoberlehner 38 | Patreon: https://www.patreon.com/maoberlehner 39 | 40 | ### License 41 | 42 | MIT 43 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | `@vue/app`, 4 | ], 5 | }; 6 | -------------------------------------------------------------------------------- /bin/server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const compression = require(`compression`); 3 | const express = require(`express`); 4 | const path = require(`path`); 5 | 6 | const PORT = 8080; 7 | 8 | const publicPath = path.join(__dirname, `..`, `dist`); 9 | const app = express(); 10 | 11 | app.use(compression()); 12 | app.use(`/`, express.static(publicPath)); 13 | // app.get(`/*`, (request, response) => response.sendFile(`${publicPath}/index.html`)); 14 | app.listen(PORT); 15 | 16 | // eslint-disable-next-line no-console 17 | console.log(`Server started!`); 18 | // eslint-disable-next-line no-console 19 | console.log(`http://127.0.0.1:${PORT}`); 20 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "npm run build" 3 | publish = "dist" 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "how-to-pre-render-vue-powered-websites-with-webpack", 3 | "version": "1.0.0", 4 | "description": "A Vue.js project", 5 | "author": "Markus Oberlehner ", 6 | "private": true, 7 | "scripts": { 8 | "serve": "vue-cli-service serve", 9 | "serve-production": "node bin/server.js", 10 | "build": "vue-cli-service build", 11 | "lint": "vue-cli-service lint" 12 | }, 13 | "dependencies": { 14 | "contentful": "^6.1.1", 15 | "vue": "^2.5.16", 16 | "vue-router": "^3.0.1", 17 | "vuex": "^3.0.1" 18 | }, 19 | "devDependencies": { 20 | "@avalanche/eslint-config": "^2.0.0", 21 | "@vue/cli-plugin-babel": "^3.0.0-rc.3", 22 | "@vue/cli-plugin-eslint": "^3.0.0-rc.3", 23 | "@vue/cli-service": "^3.0.0-rc.3", 24 | "eslint-plugin-import": "^2.13.0", 25 | "express": "^4.16.3", 26 | "node-sass": "^4.9.0", 27 | "prerender-spa-plugin": "^3.2.1", 28 | "sass-loader": "^7.0.3", 29 | "vue-template-compiler": "^2.5.16" 30 | }, 31 | "browserslist": [ 32 | "> 1%", 33 | "last 2 versions", 34 | "not ie <= 8" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maoberlehner/how-to-pre-render-vue-powered-websites-with-webpack/182b1051683e44b1e93081bd3878c8764c57b578/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | How to Pre-render Vue.js Powered Websites With webpack 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/components/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | 18 | 25 | 26 | -------------------------------------------------------------------------------- /src/components/app/AppContentBlock.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 53 | 54 | 84 | -------------------------------------------------------------------------------- /src/components/app/AppTeaser.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 54 | -------------------------------------------------------------------------------- /src/components/layouts/LayoutDefault.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 37 | 38 | 47 | -------------------------------------------------------------------------------- /src/components/views/About.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 67 | 68 | 97 | -------------------------------------------------------------------------------- /src/components/views/Home.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 71 | 72 | 101 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | import injectInitialState from './utils/inject-initial-state'; 4 | import router from './router'; 5 | import store from './store'; 6 | 7 | import App from './components/App.vue'; 8 | 9 | const app = new Vue({ 10 | router, 11 | store, 12 | render: h => h(App), 13 | }); 14 | 15 | // During pre-rendering the initial state is 16 | // injected into the global scope, here we 17 | // fill the store with the initial state. 18 | // eslint-disable-next-line no-underscore-dangle 19 | if (window.__INITIAL_STATE__) store.replaceState(window.__INITIAL_STATE__); 20 | 21 | router.beforeResolve(async (to, from, next) => { 22 | try { 23 | const components = router.getMatchedComponents(to); 24 | 25 | // By using `await` we make sure to wait 26 | // for the API request made by the `fetch()` 27 | // method to resolve before rendering the view. 28 | await Promise.all(components.map(x => x.fetch && x.fetch({ store }))); 29 | 30 | // The `injectInitialState()` function injects 31 | // the current state as a global variable 32 | // `__INITIAL_STATE__` if the page is currently 33 | // pre-rendered. 34 | // eslint-disable-next-line no-underscore-dangle 35 | if (window.__PRERENDER_INJECTED) injectInitialState(store.state); 36 | } catch (error) { 37 | // This is the place for error handling in 38 | // in case the API request fails for example. 39 | // eslint-disable-next-line no-console 40 | console.log(error); 41 | } 42 | 43 | return next(); 44 | }); 45 | 46 | app.$mount(`#app`); 47 | -------------------------------------------------------------------------------- /src/models/content-block.js: -------------------------------------------------------------------------------- 1 | import { responseAdapter as imageResponseAdapter } from './image'; 2 | 3 | export class ContentBlock { 4 | constructor({ 5 | id = null, 6 | image = ``, 7 | position = ``, 8 | text = ``, 9 | title = ``, 10 | } = {}) { 11 | this.id = id; 12 | this.image = image; 13 | this.position = position; 14 | this.text = text; 15 | this.title = title; 16 | } 17 | } 18 | 19 | export function responseAdapter(response) { 20 | const { fields, sys } = response.sys.type === `Array` 21 | ? response.items[0] 22 | : response; 23 | 24 | const image = imageResponseAdapter(fields.image); 25 | 26 | return new ContentBlock({ ...fields, ...sys, image }); 27 | } 28 | -------------------------------------------------------------------------------- /src/models/image.js: -------------------------------------------------------------------------------- 1 | export class Image { 2 | constructor({ 3 | id = null, 4 | url = ``, 5 | } = {}) { 6 | this.id = id; 7 | this.url = url; 8 | } 9 | } 10 | 11 | export function responseAdapter({ fields, sys }) { 12 | return new Image({ ...sys, url: fields.file.url }); 13 | } 14 | -------------------------------------------------------------------------------- /src/models/landing-page.js: -------------------------------------------------------------------------------- 1 | import api from '../utils/api'; 2 | import { responseAdapter as contentBlockResponseAdapter } from './content-block'; 3 | import { responseAdapter as teaserResponseAdapter } from './teaser'; 4 | 5 | export const ABOUT = `6naExkUDAIYwkSswYoQcY0`; 6 | // This is the ID of the landing 7 | // page we've created earlier. 8 | export const HOME = `7D8zXfigvuaWiK0IASKiO2`; 9 | 10 | // The LandingPage class returns a clean 11 | // LandingPage object with only the data we need. 12 | export class LandingPage { 13 | constructor({ 14 | contentBlocks = [], 15 | id = null, 16 | intro = ``, 17 | teasers = ``, 18 | title = ``, 19 | } = {}) { 20 | this.contentBlocks = contentBlocks; 21 | this.id = id; 22 | this.intro = intro; 23 | this.teasers = teasers; 24 | this.title = title; 25 | } 26 | } 27 | 28 | // We use an adapter to bring the API response 29 | // from the Contentful API into the correct format 30 | // for our LandingPage class. 31 | export function responseAdapter(response) { 32 | const { fields, sys } = response.sys.type === `Array` 33 | ? response.items[0] 34 | : response; 35 | 36 | const contentBlocks = fields.contentBlocks 37 | .map(x => contentBlockResponseAdapter(x)); 38 | const teasers = fields.teaser 39 | .map(x => teaserResponseAdapter(x)); 40 | 41 | return new LandingPage({ 42 | ...fields, 43 | ...sys, 44 | contentBlocks, 45 | teasers, 46 | }); 47 | } 48 | 49 | // We wrap the Contentful API client to format 50 | // the response exactly the way we like it. 51 | export default { 52 | async get(id) { 53 | return responseAdapter(await api.getEntries({ 'sys.id': id })); 54 | }, 55 | }; 56 | -------------------------------------------------------------------------------- /src/models/teaser.js: -------------------------------------------------------------------------------- 1 | import { responseAdapter as imageResponseAdapter } from './image'; 2 | 3 | export class Teaser { 4 | constructor({ 5 | id = null, 6 | image = ``, 7 | link = ``, 8 | text = ``, 9 | title = ``, 10 | } = {}) { 11 | this.id = id; 12 | this.image = image; 13 | this.link = link; 14 | this.text = text; 15 | this.title = title; 16 | } 17 | } 18 | 19 | export function responseAdapter(response) { 20 | const { fields, sys } = response.sys.type === `Array` 21 | ? response.items[0] 22 | : response; 23 | 24 | const image = imageResponseAdapter(fields.image); 25 | 26 | return new Teaser({ ...fields, ...sys, image }); 27 | } 28 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router from 'vue-router'; 3 | 4 | const Home = () => import(`./components/views/Home.vue`); 5 | const About = () => import(`./components/views/About.vue`); 6 | 7 | Vue.use(Router); 8 | 9 | export default new Router({ 10 | routes: [ 11 | { 12 | path: `/`, 13 | name: `home`, 14 | component: Home, 15 | }, 16 | { 17 | path: `/about`, 18 | name: `about`, 19 | component: About, 20 | }, 21 | ], 22 | mode: `history`, 23 | }); 24 | -------------------------------------------------------------------------------- /src/store/action-types.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/prefer-default-export 2 | export const GET_LANDING_PAGE = `GET_LANDING_PAGE`; 3 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | 4 | Vue.use(Vuex); 5 | 6 | export default new Vuex.Store(); 7 | -------------------------------------------------------------------------------- /src/store/modules/landing-page.js: -------------------------------------------------------------------------------- 1 | import { GET_LANDING_PAGE } from '../action-types'; 2 | import { SET_LANDING_PAGE } from '../mutation-types'; 3 | import landingPageModel, { LandingPage } from '../../models/landing-page'; 4 | 5 | export default { 6 | namespaced: true, 7 | actions: { 8 | async [GET_LANDING_PAGE]({ commit }, id) { 9 | commit(SET_LANDING_PAGE, await landingPageModel.get(id)); 10 | }, 11 | }, 12 | mutations: { 13 | [SET_LANDING_PAGE](state, landingPage) { 14 | Object.assign(state, landingPage); 15 | }, 16 | }, 17 | // We initialize the state with 18 | // an empty LandingPage object. 19 | state: () => new LandingPage(), 20 | }; 21 | -------------------------------------------------------------------------------- /src/store/mutation-types.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/prefer-default-export 2 | export const SET_LANDING_PAGE = `SET_LANDING_PAGE`; 3 | -------------------------------------------------------------------------------- /src/utils/api.js: -------------------------------------------------------------------------------- 1 | import { createClient } from 'contentful'; 2 | 3 | export default createClient({ 4 | space: `n2fxef4hydn9`, 5 | accessToken: `14aee5a3c3d231330df1e1c67927059f2280669e615184b69b7eaba4f0aed0ac`, 6 | }); 7 | -------------------------------------------------------------------------------- /src/utils/inject-initial-state.js: -------------------------------------------------------------------------------- 1 | export default function injectInitialState(state) { 2 | const script = document.createElement(`script`); 3 | script.innerHTML = `window.__INITIAL_STATE__ = ${JSON.stringify(state)}`; 4 | document.head.appendChild(script); 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/register-store-module.js: -------------------------------------------------------------------------------- 1 | export default function registerStoreModule({ module, name, store }) { 2 | // eslint-disable-next-line no-underscore-dangle 3 | const moduleIsRegistered = store._modules.root._children[name] !== undefined; 4 | const preserveState = store.state[name] !== undefined; 5 | 6 | if (!moduleIsRegistered) store.registerModule(name, module, { preserveState }); 7 | } 8 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require(`path`); 2 | const PrerenderSpaPlugin = require(`prerender-spa-plugin`); 3 | 4 | const productionPlugins = [ 5 | new PrerenderSpaPlugin({ 6 | staticDir: path.join(__dirname, `dist`), 7 | routes: [`/`, `/about`], 8 | postProcess(renderedRoute) { 9 | // eslint-disable-next-line no-param-reassign 10 | renderedRoute.html = renderedRoute.html 11 | .replace(/