Migration of Sitecore Vue Website to Nuxt

Bogdan Druziuk
Bogdan Druziuk
Cover Image for Migration of Sitecore Vue Website to Nuxt

Nuxt.js is a very popular and effective Vue framework that boosts the natural Vue.js functionality by server-side rendering, code splitting, and adhering to best practices in structuring and architecting large-scale applications

Sitecore JSS has the template for Vue.js, React, and Angular. When your site is based on React, you can easily migrate it to Next.js using an official Sitecore Next.js SDK. Nuxt.js looks like one of the best tools that can cover the same requirements for Vue.js. There is no official SDK for Nuxt, but that doesn’t mean that we can’t use it!

The following steps will help you to convert the Vue.js Sitecore JSS project to the Nuxt.js JSS project:

  1. Installation of empty Nuxt.js project. The simplest way is to create an empty Nuxt.js project and move the Vue functionality one by one instead of injecting Nuxt.js into an existing project. It makes the migration process clearer and graduated.
  npx nuxi@latest init vue-nuxtjs-migration

Nuxt Init

  1. Copying Vue project code. We should copy application sources, bootstrap and code generation scripts, and Sitecore/Vue configs.
  • ./src/* - Vue.js component and code (components, layouts, libs, routings, etc.)
  • ./scripts/* - project bootstrap and code generation
  • ./scjssconfig.json - Sitecore JSS config
  • ./vue.config.js - Vue.js settings
  1. Updating package.json. We should move the config section from the Vue.js application because some parts of the code have dependencies on these config properties. To disable the conflict between packages we should change the application type and. The npm tasks should be updated by extension of Nuxt.js tasks (including tasks related to server-side generation)
  • copy config section from Vue.js project by Sitecore config properties: paths and names
  "config": {
    "appName": "vue-nuxtjs-migration",
    "rootPlaceholders": [
      "jss-main"
    ],
    "sitecoreDistPath": "/dist/vue-nuxtjs-migration",
    "sitecoreConfigPath": "/App_Config/Include/zzz",
    "graphQLEndpointPath": "/sitecore/api/graph/edge",
    "buildArtifactsPath": "./dist",
    "language": "en",
    "templates": [
      "vue"
    ]
  },
  • remove "type": "module"
  • update app name “vue-nuxtjs-migration”
  • update npm tasks. instead of Vue.js tasks, we should use Nuxt.js tasks. For server-side generation, we should add additional tasks
  "start:connected": "npm-run-all --serial bootstrap nuxt:dev start:watch-components",
  "bootstrap": "node scripts/bootstrap.js",
  "start:vue": "vue-cli-service serve --open --host localhost",
  "start:watch-components": "node scripts/generate-component-factory.js --watch",
  "build": "npm-run-all --serial bootstrap nuxt:generate",
  "nuxt:build": "nuxi build",
  "nuxt:dev": "nuxi dev",
  "nuxt:generate": "nuxi generate",
  "nuxt:preview": "nuxi preview",
  "nuxt:postinstall": "nuxi prepare"
  • install dependencies (Bootstrap, JSS Vue, and Vue Meta from Vue.js project and Sass and Nuxt.js i18n for new functionality.
  npm i @sitecore-jss/sitecore-jss-vue @nuxtjs/i18n bootstrap vue-meta@3.0.0-alpha.7 sass --save
  • install dev dependencies ( constant-case is a small dependency in the Vue.js project. Specific version is required)
  npm i npm-run-all constant-case@3.0.4 --save-dev
  1. Adoption localization for Nuxt.js. Vue.js has a configuration of the i18n translation package that is usual for single-page applications(SPA). It uses the JSS dictionary service for getting data from Sitecore. Nuxt.js allows to use of i18n in Nuxt.js implementation using @nuxtjs/i18n npm package. It requires to be included as a module in nuxt.config and configuration in i18n.config.ts.
  • add i18n.config.ts
  import { dictionaryServiceFactory } from './lib/dictionary-service-factory';
  import config from './temp/config'
      
  export default defineI18nConfig(async () => {
    let initLanguage = config.defaultLanguage;
    const dictionaryServiceInstance = dictionaryServiceFactory.create();
          
    return dictionaryServiceInstance.fetchDictionaryData(initLanguage).then((phrases) => {
      return {
        fallbackLocale: false,
        messages: {
          [initLanguage]: phrases,
        },
        locale: initLanguage,
      }
    })
  })
  • remove ./i18n.js
  • set the app source folder
  srcDir: "src",
  • set localization config in ./nuxt.config.ts
  modules: [
    '@nuxtjs/i18n',
  ],
  i18n: {
    vueI18n: './src/i18n.config.ts'
  }
  1. Adoption of app structure to Nuxt.js approach. Nuxt.js has automatic routing, using /pages folder for saving the pages list and resolving the app entry point form ./src/ root folder that was created before.
  • remove ./app.vue. The entry point of Nuxt.js is ./src folder so we don’t need the app in the root anymore.
  • rename ./src/AppRoot.vue to ./src/app.vue which will be used as a new application entry point
  • create ./src/pages folder
  • move ./RouteHandler.vue to ./src/pages and rename
  • remove ./src/main.js as rudimental app creation
  • remove ./src/router.js which will be replaced by Nuxt.js automatic routing
  • remove ./src/registerServiceWorker.js
  • remove ./src/createApp.js
  1. Creation of Nuxt.js plugin for app initialization. Vue.js has a natural starting point for SPA. In Nuxt.js the same initial data is possible to pass by the plugin. We should move from removed createApp.js the next initialization functionality: SitecoreJssStorePlugin - Vue.js store for context data; SitecoreJssPlaceholderPlugin - Vue.js JSS placeholder/components resolving plugin; createMetaManager - Vue.js Vue Meta plugin.
  import componentFactory from '../temp/componentFactory';
  import { SitecoreJssPlaceholderPlugin } from '@sitecore-jss/sitecore-jss-vue';
  import SitecoreJssStorePlugin from '../lib/SitecoreJssStorePlugin';
  import { createMetaManager } from 'vue-meta'
  
  export default defineNuxtPlugin({
    name: 'init',
    enforce: 'pre', // or 'post'
    async setup (nuxtApp) {
      const app = nuxtApp.vueApp;
      app.use(SitecoreJssStorePlugin);
      app.use(SitecoreJssPlaceholderPlugin, { componentFactory });
      app.use(createMetaManager())
    },
    env: {
      // Set this value to `false` if you don't want the plugin to run when rendering server-only or island components.
      islands: true
    }
  })
  1. Injecting Nuxt.js NuxtPage. The Vue.js uses the <router-view /> but in the Nuxt.js we should use <NuxtPage />. for resolving routing/pages.

Nuxt page

  1. Resolving page data by routing. We need to update […path].vue page to work together with Sitecore. It should resolve all routes and get the data and translation for them
  • resolving routing and language by layout service. Return actual data for this page
  async setup() {
    const app = useNuxtApp().vueApp;
    const params = useRoute().params;
          
    let resolveParams = function(params) {
      let language = config.defaultLanguage;
      let pathStr = "/";
      
      if (params.path) {
        pathStr = params.path.join("/");
      }
      
      if (params.path[0] && packageConfig.config.locales.map(v => v.toLowerCase()).includes(params.path[0].toLocaleLowerCase())) {
        language = params.path[0];
        pathStr = params.path.slice(1).join("/");
      }
      
      let sitecoreRoutePath = pathStr ? pathStr : "/";
      if (!sitecoreRoutePath.startsWith("/")) {
        sitecoreRoutePath = `/${sitecoreRoutePath}`;
      }
      
      return { sitecoreRoutePath, language };
    }
      
    const { sitecoreRoutePath, language } = resolveParams(params);
      
    const layoutServiceInstance = layoutServiceFactory.create();
      
    const { data: result } = await useAsyncData("routeData", () =>
      layoutServiceInstance.fetchLayoutData(sitecoreRoutePath, language)
    );
      
    const routeData = result.value;
    app.config.globalProperties.$jss.store.setSitecoreData(routeData);
      
    return { routeData };
  },
  • resolving routing, and language by layout service. Return actual data for this page
  created() {
    const app = useNuxtApp().vueApp
    this.appState = app.config.globalProperties.$jss.store.state
  },
  1. Replace Links. Replace all <router-link> and <a> to <NuxtLink> with “external” property for correct static site generation

Nuxt links

  1. Adoption of the metainfo component. Add <metainfo> component to <client-only> tag to render its slot only in client-side.

Metainfo

  1. Adding Sitemap Service for correct static page generation.
  • Add ./src/lib/sitemap-service.ts
  import type { GraphQLClient, PageInfo } from "@sitecore-jss/sitecore-jss/graphql";
  import * as jss from "@sitecore-jss/sitecore-jss";
  import GraphQLRequestClientFactory from "./graphql-request-client-factory";
  import config from "../temp/config";
  const { debug } = jss;
      
  export type StaticPath = {
    params: {
      path?: string;
    }
  };
   
  export type RouteQueryResult = {
    path: string;
  };
      
  export type SiteRouteQueryResult<T> = {
    site: {
      siteInfo: {
        routes: {
          pageInfo: PageInfo;
            results: T[];
          }
        }
      }
    }
      
    export interface ISitemapService {
      getStaticSitemap(language: string): Promise<StaticPath[]>;
    }
      
    type SitemapQueryArgs = {
      siteName: string;
      language: string;
      pageSize: number;
    };
      
    export class SitemapService implements ISitemapService {
      private readonly _siteMapQuery: string = `
      query DefaultSitemapQuery(
        $siteName: String!
        $language: String!
        $pageSize: Int = 10
        $after: String
      ) {
        site {
          siteInfo(site: $siteName) {
            routes(
              language: $language
              first: $pageSize
              after: $after
            ){
              total
              pageInfo {
                endCursor
                hasNext
              }
              results {
                path: routePath            
              }
            }
          }
        }
      }
      `;
      
      private readonly _pageSize: number = 20;
      
      private _graphQlClient: GraphQLClient
      
      constructor(graphQlClientFactory: GraphQLRequestClientFactory) {
        this._graphQlClient = graphQlClientFactory.create(debug.sitemap);
      }
      
      async getStaticSitemap(language: string): Promise<StaticPath[]> {
        const routes: RouteQueryResult[] = await this.fetchSitemap(language);
          
        return routes.filter(route => {
          if(route.path) {
            return true;
          }
      
          return false;
        }).map(route => {
          let normalized:string|undefined = undefined;
          if(route.path !== "/") {
            normalized = route.path.replace(/^\/|\/$/g, "");
          }
      
          return {
            params: {
              path: normalized,
            }
          };
        });
      }
      
      protected async fetchSitemap(language: string): Promise<RouteQueryResult[]> {
        const args: SitemapQueryArgs = {
          siteName: config.jssAppName,
          language: language,
          pageSize: this._pageSize
        };
      
        let result: RouteQueryResult[] = [];
        let hasNext = true;
        let after = "";
      
        while(hasNext) {
          const response = await this._graphQlClient.request<SiteRouteQueryResult<RouteQueryResult>>(this._siteMapQuery, { ...args, after });
          result = result.concat(response.site.siteInfo?.routes.results);
          hasNext = response.site.siteInfo.routes?.pageInfo.hasNext;
          after = response.site.siteInfo.routes?.pageInfo.endCursor;
        }
      
        return result;
      }
  }
  • Add ./src/lib/graphql-request-client-factory.ts
  import * as jss from "@sitecore-jss/sitecore-jss/graphql";
  import type { GraphQLRequestClientConfig } from "@sitecore-jss/sitecore-jss/graphql";
  import type { Debugger } from "@sitecore-jss/sitecore-jss";
  import config from "../temp/config";
  const { GraphQLRequestClient } = jss;
      
  export default class GraphQLRequestClientFactory {
    create(debug: Debugger): any {
      const graphQlConfig: GraphQLRequestClientConfig = {
        apiKey: config.sitecoreApiKey,
        debugger: debug
      };
      
        return new GraphQLRequestClient(config.graphQLEndpoint, graphQlConfig);
    }
  }
  • Update nuxt.config
  import config from "./src/temp/config";
  import packageConfig from './package.json';
  import { SitemapService } from "./src/lib/sitemap-service";
  import GraphQLRequestClientFactory from "./src/lib/graphql-request-client-factory";
      
  const graphQlRequestSitemapFactory = new GraphQLRequestClientFactory();
  const sitemapService = new SitemapService(graphQlRequestSitemapFactory);
      
  const getRoutes = async () => {
    const defaultRoutes = await sitemapService.getStaticSitemap(config.defaultLanguage);
        
    let defaultStaticPaths = defaultRoutes.map((route) => {
      return route.params.path ? `/${route.params.path}` : "/"
    })
      
    let allPaths = defaultStaticPaths
      
    var filteredlocales = packageConfig.config.locales?.map(v => v.toLowerCase()).filter(x => x !== config.defaultLanguage.toLocaleLowerCase())
      
    if (filteredlocales && filteredlocales.length > 0) {
      
      for (const locale of filteredlocales) {
        const localeRoutes = await sitemapService.getStaticSitemap(locale);
            
        let localeStaticPaths = localeRoutes.map((route) => {
          return `/${locale}/${route.params.path ? route.params.path : ""}`
        })
      
        allPaths.push(...localeStaticPaths);
      }
    }
      
    return allPaths;
  };
      
  // https://nuxt.com/docs/api/configuration/nuxt-config
  export default defineNuxtConfig({
    srcDir: "src",
    devtools: { enabled: true },
    modules: [
      '@nuxtjs/i18n',
    ],
    i18n: {
      vueI18n: './src/i18n.config.ts'
    },
    vite: {
      build: {
        commonjsOptions: {
          include: ["**/*.js"]
        }
      }
    },
    experimental: {
      asyncContext: true
    },
    hooks: {
      async 'nitro:config'(nitroConfig) {
        const routes = await getRoutes();
        nitroConfig?.prerender?.routes?.push(...routes);
      },
    },
  })
  1. Fixing style order issue. The order of imported css/scss files changes by default algorithm in Nuxt.js engine. To fix this issue we should relate one css/scss file and import all other styles inside.

Styles fix

  1. Fixing config importing issue. temp/config.js generates conflicts in some components of the file. For fast resolving, we can use import * as format

Config fix

  1. Fixing the Content Links issue.

Richtext fix

  1. Fix Tabs rendering issue.

Keep alive

Conclusion

Adding Nuxt.js to the existing Sitecore JSS Vue.js website is not as straightforward as adding Astro. But it is still achievable. You will be able to get statically rendered pages with some level of effort. Your level of abilities will be comparable with what Sitecore provides in the official JSS Next.js SDK.

Do you have a Sitecore JSS Vue.js website and want to get more from it? Feel free to contact us. We will be able to discuss the possible options with Nuxt and Astro, and select and implement what is better for your case.