Вопрос: React/vue и cms?

Лучший ответ:
Погуглите Headless CMS.
Очень многие популярные CMS имеют свою headless версию, т е есть админка и есть REST API, которая отдается на основе данных, занесенных в эту админку.

Соответственно, морду вы можете писать на чем хотите: Angular, React, Vue и т д
И даже вообще можете разместить ее на другом домене.

Как вообще клиенты потом меняют содержимое сайта если нет cms, т.е есть только к примеру бэкенд на php, и фронт на реакте или вью?
На том же реакте тоже можно написать админку и через нее обновлять данные.

Худший ответ:
Вот пример роутера для vue
// Imports
import Vue from 'vue'
import Router from 'vue-router'
import { authGuard, logoutHabdler } from '@/helpers'

Vue.use(Router)

const router = new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  scrollBehavior: (to, from, savedPosition) => {
    if (to.hash) return { selector: to.hash }
    if (savedPosition) return savedPosition

    return { x: 0, y: 0 }
  },
  routes: [
    {
      path: '/logout',
      beforeEnter: logoutHabdler,
    },
    {
      path: '/',
      component: () => import('@/layouts/home/Index.vue'),
      children: [
        {
          path: '',
          name: 'Home',
          component: () => import('@/views/home/Index.vue'),
        },

        {
          path: 'about',
          name: 'About',
          component: () => import('@/views/about/Index.vue'),
        },
        {
          path: 'description',
          name: 'Description',
          component: () => import('@/views/description/Index.vue'),
        },
        {
          path: 'price',
          name: 'Price',
          component: () => import('@/views/price/Index.vue'),
        },
        {
          path: 'login',
          name: 'Login',
          component: () => import('@/views/login/Index.vue'),
        },
        {
          path: 'legal',
          name: 'Legal',
          component: () => import('@/views/home/Legal.vue'),
        },
        {
          path: 'eula',
          name: 'Eula',
          component: () => import('@/views/home/Eula.vue'),
        },
        {
          path: 'condition',
          name: 'Condition',
          component: () => import('@/views/home/Condition.vue'),
        },
        {
          path: 'disclosure/:id?',
          name: 'Disclosure',
          component: () => import('@/views/home/Disclosure.vue'),
        },
        {
          path: 'userlist/:id?',
          name: 'UserList',
          component: () => import('@/views/home/UserList.vue'),
        },
        {
          path: 'open/:id?',
          name: 'SuccessorList',
          component: () => import('@/views/home/SuccessorList.vue'),
        },
      ],
    },
    {
      path: '/user',
      component: () => import('@/layouts/user/Index.vue'),
      beforeEnter: authGuard,
      children: [
        {
          path: '',
          name: 'UserHome',
          component: () => import('@/views/user/Index.vue'),
        },
        {
          path: 'store/:id?',
          name: 'UserStore',
          component: () => import('@/views/user/Store.vue'),
        },
        {
          path: 'faq',
          name: 'UserFaq',
          component: () => import('@/views/user/Faq.vue'),
        },
        {
          path: 'messages',
          name: 'UserMessages',
          component: () => import('@/views/user/Messages.vue'),
        },
        {
          path: 'contacts',
          name: 'UserContacts',
          component: () => import('@/views/user/Contacts.vue'),
        },
        {
          path: 'profile',
          name: 'UserProfile',
          component: () => import('@/views/user/Profile.vue'),
        },
        {
          path: 'payment/:id',
          name: 'Payment',
          component: () => import('@/views/user/Payment.vue'),
        },
      ],
    },
    {
      path: '*',
      component: () => import('@/layouts/user/Page404.vue'),
      children: [
        {
          path: '*',
          name: 'Page404',
          component: () => import('@/views/user/Page404.vue'),
        },
       ],
  },
  ],
})

export default router

Вот конкретно этот кусок обрабатывает пути /user/store/какойто_идентификатор
{
          path: 'store/:id?',
          name: 'UserStore',
          component: () => import('@/views/user/Store.vue'),
        },

Соответственно вы можете создать путь для ваших страниц

далее 
vue.config.js

const path = require('path')
const PrerenderSPAPlugin = require('@dreysolano/prerender-spa-plugin')
const Renderer = PrerenderSPAPlugin.PuppeteerRenderer
const isDev = process.env.NODE_ENV !== 'production'
const plugins = isDev
  ? []
  : [
      new PrerenderSPAPlugin({
        // Required - The path to the webpack-outputted app to prerender.
        staticDir: path.join(__dirname, 'dist'),

        // Optional - The path your rendered app should be output to.
        // (Defaults to staticDir.)
        // outputDir: path.join(__dirname, 'prerendered'),

        // Optional - The location of index.html
        indexPath: path.join(__dirname, 'dist', 'index.html'),

        // Required - Routes to render.
        // Генерируйте пути из бд и билдите приложение какким нибудь скриптом на основе базы данных
        routes: ['/', '/about', '/description', '/price', '/about'],

        // Optional - Allows you to customize the HTML and output path before
        // writing the rendered contents to a file.
        // renderedRoute can be modified and it or an equivelant should be returned.
        // renderedRoute format:
        // {
        //   route: String, // Where the output file will end up (relative to outputDir)
        //   originalRoute: String, // The route that was passed into the renderer, before redirects.
        //   html: String, // The rendered HTML for this route.
        //   outputPath: String // The path the rendered HTML will be written to.
        // }
        postProcess (renderedRoute) {
          // Ignore any redirects.
          renderedRoute.route = renderedRoute.originalRoute
          // Basic whitespace removal. (Don't use this in production.)
          // renderedRoute.html = renderedRoute.html.split(/>[\s]+</gim).join('><');
          // Remove /index.html from the output path if the dir name ends with a .html file extension.
          // For example: /dist/dir/special.html/index.html -> /dist/dir/special.html
          if (renderedRoute.route.endsWith('.html')) {
            renderedRoute.outputPath = path.join(
              __dirname,
              'dist',
              renderedRoute.route,
            )
          }

          return renderedRoute
        },

        // Optional - Uses html-minifier (https://github.com/kangax/html-minifier)
        // To minify the resulting HTML.
        // Option reference: https://github.com/kangax/html-minifier#options-quick-reference
        minify: {
          collapseBooleanAttributes: true,
          collapseWhitespace: true,
          decodeEntities: true,
          keepClosingSlash: true,
          sortAttributes: true,
        },

        // Server configuration options.
        server: {
          // Normally a free port is autodetected, but feel free to set this if needed.
          port: 8001,
        },

        // The actual renderer to use. (Feel free to write your own)
        // Available renderers: https://github.com/Tribex/prerenderer/tree/master/renderers
        renderer: new Renderer({
          // Optional - The name of the property to add to the window object with the contents of `inject`.
          injectProperty: '__PRERENDER_INJECTED',
          // Optional - Any values you'd like your app to have access to via `window.injectProperty`.
          inject: {
            foo: 'bar',
            prerender: false,
          },

          // Optional - defaults to 0, no limit.
          // Routes are rendered asynchronously.
          // Use this to limit the number of routes rendered in parallel.
          maxConcurrentRoutes: 4,

          // Optional - Wait to render until the specified event is dispatched on the document.
          // eg, with `document.dispatchEvent(new Event('custom-render-trigger'))`
          // renderAfterDocumentEvent: 'custom-render-trigger',

          // Optional - Wait to render until the specified element is detected using `document.querySelector`
          renderAfterElementExists: '#app',

          // Optional - Wait to render until a certain amount of time has passed.
          // NOT RECOMMENDED
          renderAfterTime: 5000, // Wait 5 seconds.

          // Other puppeteer options.
          // (See here: https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#puppeteerlaunchoptions)
          headless: true, // Display the browser window when rendering. Useful for debugging.
        }),
      }),
    ]

module.exports = {

  devServer: {
    disableHostCheck: true,
    progress: false,
    before () { // Output the same message as the react dev server to get the Spa middleware working with vue.
      console.info('Starting the development server...')
    },
  },
  transpileDependencies: ['vuetify'],

  configureWebpack: {
    plugins: [...plugins],
  },
}

В итоге получите SPA приложение с поисковой оптимизацией