Building Server-side Rendered Vue.js Apps Using Nuxt.js

Share

Nuxt.js is often regarded as an excellent solution for server-side rendering, particularly for dynamic web applications built with the Vue.js framework. By leveraging server-side rendering, Nuxt.js enables these applications to be more compatible with search engines like Google and Bing, ultimately leading to significant increases in traffic.

I. Vue.js Project Structure Overview

Vue.js is the foundational JavaScript framework upon which Nuxt.js was built. Nuxt.js, often referred to as a "framework on top of Vue.js," extends Vue.js by providing additional features and conventions for building server-side rendered and static generated applications. Therefore, Nuxt.js inherits many concepts and functionalities from Vue.js while also introducing its own features and optimizations.

For example, here is the folder structure for using Vue.js in association with Laravel.

laravel_vue_app
├── app
│   ├── Http
│      ├── Controllers
│      └── ...
│   ├── Models
│   └── ...
├── resources
│   ├── js
│      ├── components
│         ├── HomeComp.vue
│         └── ...
│      ├── routes
│         └── index.js
│      └── app.js
│   └── views
│       ├── front
│          └── Home.vue
│       └── layouts
│           ├── LayoutFront
│              └── ...
│           └── partials
│               ├── Header.vue
│               └── Footer.vue
├── routes
│   └── web.php
└── views
│    └── app.blade.php
├── package.json
└── README.md

On the other hand, you can refer to the basic structure that includes the main directories and configuration files commonly found in a Vue.js Single Page Application (SPA) project.

spa_vue_app
├── public
│   └── index.html
├── src
│   ├── assets
│   ├── components
│      └── HomeComp.vue       ├── router
│      └── index.js
│   ├── store
│      └── app.js
│   └── views
│       ├── Home.vue
│       └── ...
├── .babelrc
├── .eslintrc
├── package.json
└── README.md

When setting up a Vue.js application, there are several important concepts and elements that need to be established. Typically, we go through each key item to consider:

» app.js: set up essential plugins and configurations such as Vue Router, Vuex (if used), internationalization (i18n), and custom utilities. Additionally, it configures various Vue.js plugins and filters, including a loading indicator, form builder, pagination, and syntax highlighting. It also manages HTTP request interceptors to show loading indicators during AJAX requests.

For example:

import Vue from 'vue';
window.Vue = require("vue").default;

...
new Vue({
    i18n,
    router: routerFront,
    store,
    components: {
        Loading
    },
    mounted() {
        this.enableInterceptor();

        this.vueRouteUrl = window.location.pathname;
        if (this.vueRouteUrl) {
            routerFront.push(this.vueRouteUrl);
        }
    },
    data: {
        isLoading: false,
        requestCount: 0,
        vueRouteUrl: null,
        axiosInterceptor: null,
        indicatorOptions: LoadingIndicatorConfig
    },
    methods: {
        increaseReqCount() {
            this.requestCount++;
        },
        decreaseReqCount() {
            this.requestCount--;
        },
        enableInterceptor() {
            const self = this;
            self.axiosInterceptor = Axios.interceptors.request.use(config => {
                self.increaseReqCount();
                setTimeout(function () {
                    if (self.requestCount > 0) {
                        self.isLoading = false;
                    }
                }, self.indicatorOptions.timeout);
                return config;
            }, error => {
                self.decreaseReqCount();
                self.isLoading = false;
                return Promise.reject(error);
            });
            Axios.interceptors.response.use(response => {
                self.decreaseReqCount();
                self.isLoading = (self.requestCount > 0);
                return response;
            }, error => {
                self.decreaseReqCount();
                self.isLoading = false;
                return Promise.reject(error);
            });
        },
        disableInterceptor() {
            Axios.interceptors.request.eject(this.axiosInterceptor);
        }
    },
    computed: {
        currentRequests() {
            return this.requestCount;
        }
    },
    watch: {
        requestCount: function () {
            const self = this;
            self.isLoading = self.currentRequests > 0;
        }
    }
}).$mount('#app');

» router.js: configure the Vue Router for client-side routing in the application.

For example:

import Vue from 'vue';
import VueRouter from 'vue-router';
import utils from './helpers/utilities';
import firebase from 'firebase/app';
import VueAnalytics from 'vue-analytics';

/*
 |--------------------------------------------------------------------------
 | Admin Views
 |--------------------------------------------------------------------------|
 */
window.$ = window.jQuery = require('./jquery.min.js');


import AuthService from "./services/auth-front";
import Ls from "./services/ls";
import VueCookies from 'vue-cookies';

Vue.use(VueCookies);
Vue.use(VueRouter);

const routes = [
    {
        path: '/',
        redirect: { name: 'home' },
        component: () => import(/* webpackChunkName: "10d6adf0cc20257fdfbe3e0214c6f9cd" */'./views/layouts/LayoutFront.vue'),
        children: [
            {
                path: 'home',
                name: 'home',
                component: () => import(/* webpackChunkName: "106a6c241b8797f52e1e77317b96a201" */'./views/front/Home')
            },
            ...
	}
]
...
const router = new VueRouter({
    routes,
    mode: 'history',
    linkActiveClass: 'active'
});

router.beforeEach((to, from, next) => {
    ...
});

export default router;

» Vue Component: Here is basic structure for a Vue component typically includes three main sections: template, script, and style.

For example:

<template>
  <div>
    <!-- Your HTML -->
  </div>
</template>

<script>
  export default {
    data() {},
    watch: {},
    mounted() {},
    created() {},
    methods: {}
  };
</script>
  
<style scoped>
  <!-- Your CSS -->
</style>

Here, we will outline three approaches you can take to understand and implement Server-Side Rendering (SSR) with Nuxt.js:

  1. If you currently have a Laravel application with Vue.js handling the frontend as Single Page Application (SPA), it may lack optimal Search Engine Optimization (SEO) due to the client-side rendering nature of SPAs. In this case, you would need to decouple Vue.js from Laravel and develop individual services using SSR with Nuxt.js to enhance SEO performance.
  2. If you already have an existing SPA built with Vue.js but you are not satisfied with its performance or SEO capabilities, migrating to Nuxt.js and implementing SSR could be possible solution. By transitioning to Nuxt.js, you can leverage its SSR capabilities to improve SEO and overall performance.
  3. Alternatively, if you are starting new project or want to revamp an existing Vue.js application with the goal of incorporating SSR from the outset, you can begin by initializing Nuxt.js application. From there, you can conceptualize your project and implement SSR as part of your development process.

II. Setting up Nuxt.js in the Example Application

First and foremost, let us set up the essential tools for developing Nuxt.js application. Ensure you have the following prerequisites in place:

  • Node.js: Install Node.js from the official website.
  • Package Manager: Use npm or Yarn (check compatibility with your Node.js version).
  • Code Editor: Choose Visual Studio Code, Sublime Text, Atom, or WebStorm.
  • Terminal or Command Prompt: Ensure you have access to a terminal or command prompt for executing commands.

If you are using Windows and need to switch between different versions of Node.js, you can use the nvm (Node Version Manager) tool. Follow the steps below:

nvm ls
mvn use 14.18.0

To create a new Nuxt.js project using the create-nuxt-app CLI, the command is:

npx create-nuxt-app nuxtssrsample

The command will prompt you with a series of questions to customize your Nuxt.js project, such as selecting a UI framework, choosing between SSR (Server-Side Rendering) or SPA (Single Page Application), and more. Once you've answered the questions, it will generate a new Nuxt.js project with the specified name ("nuxtssrsample" in this case) in the current directory.

After successfully generating new Nuxt.js application using the create-nuxt-app command, the folder structure typically looks like this:

nuxtssrsample/
├── .nuxt/
├── assets/
├── layouts/
├── middleware/
├── pages/
├── plugins/
├── static/
├── store/
├── .editorconfig
├── .eslintrc.js
├── .gitignore
├── nuxt.config.js
├── package.json
└── README.md

However, with the basic structure, implementing SSR can be challenging, requiring enhancements to accommodate SSR layouts. Here is an sample implementation demonstrating these enhancements.

nuxtssrsample/
├── .nuxt/
├── assets/
│   ├── constants/
│      └── config.js
│   ├── css/
│      └── seq.css    ├── font/
│      └── font.css
│   ├── helpers/
│      ├── bus.js       ├── constants.js
│      ├── highlight.js
│      └── utils.js
│   ├── images/
│      ├── tab.svg
│      └── breadcumb.svg
│   └── js/
│       ├── components/
│          ├── _blog/
│             ├── TableContent.vue
│             └── Comment.vue           └── _converter/
│              └── timezone.js
│       ├── composables/
│          └── googleOneTapSignin.js
│       └── views/
│           └── layouts/  
├── layouts/
│   ├── partials/
│      ├── AuthModal.vue
│      ├── Footer.vue
│      ├── Header.vue
│      └── Navbar.vue
│   ├── LoginModal.vue
│   ├── LogoutModal.vue
│   └── default.vue
├── middleware/
│   └── robots.js
├── pages/
│   ├── home.vue
│   ├── post/
│      └── _category/
│          └── _blog.vue
│   └── snippets.vue
│       └── _aspect/
│           └── _code.vue
├── plugins/
│   ├── sweetalert.js
│   ├── v-form-builder.js
│   ├── v-highlight.js
│   ├── v-axios.js
│   ├── vue-bootstrap.js
│   ├── vue-filter.js
│   ├── vue-firebase.js
│   ├── vue-flagtick-directive.js
│   ├── vue-gtag.js
│   ├── vue-head.js
│   ├── vue-infinite-loading.js
│   ├── vue-progressbar.js
│   ├── vue-vform.js
│   └── vue-vuelidate.js
├── static/
│   ├── css/
│      ├── all.min.css
│      ├── app.min.css
│      ├── bootstrap.min.css
│      ├── common.min.css
│      ├── flagtick.min.css
│      └── monokai.min.css
│   ├── images/
│      ├── close.png
│      ├── user.png
│      └── logo.png
│   └── js/
│       ├── bootstrap.min.js
│       └── jquery.min.js
├── store/
│   ├── api/
│      └── apiState.js
│   ├── init/
│      └── initialState.js
│   └── index.js
│   └── serverInitHandler.js
├── .babelrc
├── .editorconfig
├── .env
├── .eslintrc.js
├── .gitignore
├── nuxt.config.js
├── package.json
└── README.md

In addition, the package.json file serves as a central configuration file in Nuxt.js projects, offering a comprehensive overview of the project's dependencies, scripts, metadata, and configurations.

For instance, consider the package.json file defined below:

{
  "name": "flagtick",
  "version": "1.0.0",
  "description": "An Flagtick project built using Nuxt",
  "author": "Flagtick <[email protected]>",
  "private": true,
  "scripts": {
    "dev": "nuxt",
    "build": "nuxt build",
    "start": "nuxt start",
    "generate": "nuxt generate",
    "lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
    "precommit": "npm run lint",
    "watch": "nuxt --watch"
  },
  "dependencies": {
    "@fortawesome/fontawesome-free": "^6.4.0",
    "@fortawesome/vue-fontawesome": "^3.0.3",
    "@nuxtjs/axios": "^5.13.6",
    "@nuxtjs/fontawesome": "^1.1.2",
    "@nuxtjs/i18n": "^7.3.1",
    "@nuxtjs/router": "^1.7.0",
    "@tiptap/starter-kit": "^2.0.4",
    "@tiptap/vue-2": "^2.0.4",
    "cookie-universal-nuxt": "^2.2.2",
    "express": "^4.18.2",
    "firebase": "^7.8.2",
    "fs": "0.0.1-security",
    "he": "^1.2.0",
    "highlight.js": "^11.2.0",
    "highlightjs-line-numbers.js": "^2.8.0",
    "moment": "^2.29.4",
    "multer": "^1.4.5-lts.1",
    "nuxt": "^2.15.7",
    "nuxt-sass-resources-loader": "^2.0.5",
    ...
  },
  "devDependencies": {
    "@nuxtjs/html-validator": "^1.2.4",
    "axios": "^0.18.1",
    "babel-eslint": "^8.2.1",
    "eslint": "^4.15.0",
    "eslint-friendly-formatter": "^3.0.0",
    "eslint-loader": "^1.7.1",
    "eslint-plugin-vue": "^4.0.0",
    "node-sass": "^8.0.0",
    "sass": "^1.54.8",
    "sass-loader": "^13.2.0"
  }
}

You can run npm run dev to start the development mode of the Nuxt application, accessible at http://localhost:3000/. In the next section, we will delve into configuring the nuxt.config.js file at a high level in Nuxt applications.

III. Get Started with Nuxt Configuration file

First and foremost, let's discuss the mode in a Nuxt application, which refers to the rendering mode of the application. There are two modes:

  • Universal Mode: Mixes server-side rendering (SSR) and client-side rendering (CSR) for SEO benefits and dynamic user experiences.
  • Single Page Application (SPA) Mode: Renders the entire application on the client side with JavaScript for responsiveness but may affect SEO and initial load times.

Hence, if your target is SSR, we will use the mode universal instead of spa to enable server-side rendering, which is more SEO-friendly for search engines.

module.exports = {

  mode: 'universal',
  ...

When it comes to cookies, the question here is: How can we manage cookies in both server-side and client-side contexts in Nuxt application? or we want to integrating Font Awesome icons.

...
  buildModules: [
    ['cookie-universal-nuxt', { alias: 'cookiz' }],
    '@nuxtjs/fontawesome'
  ],
...

Note: By setting { alias: 'cookiz' }, it gives an alias name cookiz for the module, which can be used to access its functionality within the application.

Ensures all routes end with a trailing slash ("/") in Nuxt configuration. If a user accesses a route without it, the router automatically redirects to the same route with the trailing slash.

...
  router: {
    trailingSlash: true
  },
...

For example: When using the command prompt and running curl -I https://www.flagtick.com/snippet/javascript/convert-timestamp-to-iso-date-string, you can observe that a 301 Moved Permanently status is returned, indicating a redirection. However, when the URL includes a trailing slash (https://www.flagtick.com/snippet/javascript/convert-timestamp-to-iso-date-string/), a 200 OK status is returned, indicating successful access to the resource.

C:\Users\admin>curl -I https://www.flagtick.com/snippet/javascript/convert-timestamp-to-iso-date-string
HTTP/1.1 301 Moved Permanently
Date: Sat, 24 Feb 2024 13:34:05 GMT

C:\Users\admin>curl -I https://www.flagtick.com/snippet/javascript/convert-timestamp-to-iso-date-string/
HTTP/1.1 200 OK
Date: Sat, 24 Feb 2024 13:34:12 GMT
...

Specifies caching parameters in Nuxt.js application, limiting the cache to store maximum of 1000 items, with each item being valid for 15 minutes (900,000 milliseconds) before it expires.

  ...
  cache: {
    max: 1000,
    maxAge: 900000
  },

Upon running npm run generate in Nuxt.js application with SSR enabled (ssr: true), Nuxt will generate a static version of your application with pre-rendered HTML files for each route.

  ...
  ssr: true,

The target property in Nuxt.js determines how the application will be built and deployed:

  • server: Renders pages on the server side, providing benefits like SEO and initial page load performance.
  • static: Generates static HTML files at build time, suitable for hosting on static file servers or CDNs for fast and scalable delivery.
  ...
  target: 'server',

Enables both fallback mode and pre-generation of dynamic routes during the static site generation process.

  const slugs = require('./assets/ssr/slugs.json');
  ...

  generate: {
    fallback: true,
    routes: slugs
  },

Note: The slugs.json file contains an array of URLs representing routes in your Nuxt.js application. These URLs typically correspond to dynamic routes or pages that are generated based on data.

[
    "/home/",
    ...
]

If your server receives URLs that use Unicode escape sequence (\u002F) instead of forward slash (/), and you want to normalize the URLs before they are processed by the Nuxt.js application, the provided serverMiddleware configuration allows you to achieve that normalization by replacing occurrences of the Unicode escape sequence with the forward slash in the URL (req.url).

  ...
  serverMiddleware: [
    (req, res, next) => {
      req.url = req.url.replace(/\\u002F/g, '/');
      next();
    }
  ],

Use this hook targets the 'vue-renderer:ssr:context' event, which occurs during the server-side rendering (SSR) process of Nuxt.js. It intercepts the rendering context object (context) and applies custom modifications before the rendering process completes.

  ...
  hooks: {
    'vue-renderer:ssr:context'(context) {
      const routePath = JSON.stringify(context.nuxt.routePath);
      context.nuxt = {serverRendered: true, routePath};
    }
  },

Leverage the <head> element for each page of your Nuxt.js application to enable specification of the title, meta tags, links to stylesheets, and scripts to be included in the generated HTML.

    ...	
    head: {
		title: 'The Innovative Blog for Programmers',
		htmlAttrs: {
		  lang: 'en'
		},
		meta: [
		  { charset: 'utf-8' },
		  { name: 'locale', content: 'en' },
		  { name: 'viewport', content: 'width=device-width, initial-scale=1' }
		],
		link: [
		  { rel: 'stylesheet', type: 'text/css', href: '/css/monokai.min.css'},
		  { rel: 'stylesheet', type: 'text/css', href: '/css/bootstrap.min.css'},
		  { rel: 'stylesheet', type: 'text/css', href: '/css/common.min.css'},
		  { rel: 'stylesheet', type: 'text/css', href: '/css/flagtick.min.css'},
		  { rel: 'stylesheet', type: 'text/css', href: '/css/all.min.css'},
		  { rel: 'stylesheet', type: 'text/css', href: '/css/app.min.css'},
		  { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
		],
		script: [
		  {
			src: "/js/jquery.min.js",
			type: "text/javascript"
		  },
		  {
			src: "/js/popper.min.js",
			type: "text/javascript"
		  },
		  {
			src: '/js/bootstrap.min.js',
			type: "text/javascript"
		  }
		]
	},

To enhance the functionality of your Nuxt.js application, register JavaScript plugins using the plugins configuration. These plugins can extend Vue.js or provide additional features to your application.

 ... 
 plugins: [
    { src: '~/plugins/vue-head' },
    { src: '~/plugins/vue-axios' },
    { src: '~/plugins/vue-vform' },
    { src: '~/plugins/vue-vuelidate' },
    { src: '~/plugins/vue-infinite-loading' },
    { src: '~/plugins/vue-gtag.js', mode: 'client' },
    { src: '~/plugins/vue-progressbar.js', mode: 'client' },
    { src: '~/plugins/v-form-builder.js', mode: 'client' },
    { src: '~/plugins/vue-flagtick-directive.js', mode: 'client' },
    { src: '~/plugins/vue-filters.js', mode: 'client' },
    { src: '~/plugins/sweetalert.js', mode: 'client' }
  ],

Note: By using mode: 'client' in the plugins configuration in Nuxt.js, it indicates that the plugin is intended to run on the client side (in the browser) and is excluded during server-side rendering (SSR).

For example, vue-gtag.js utilizes the window and document objects, which is why we exclude it using mode: 'client'.

export default ({ app }) => {
  const script = document.createElement('script')
  script.async = true
  script.src = 'https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXX'
  document.head.appendChild(script)

  window.dataLayer = window.dataLayer || []
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date())
  gtag('config', 'G-XXXXXXXXX')
}

The loading configuration in Nuxt.js is used to customize the loading indicator that appears when navigating between pages or when the application is loading data.

  ...  
  loading: { color: '#3B8070', sse: true },

Finally, optimize your application's build output by extracting CSS, managing build warnings, and customizing HTML minification options to reduce file size and enhance performance.

build: {
  extractCSS: true,
  warnings: false,
  html: {
    minify: {
      collapseWhitespace: true,
      removeComments: true,
      minifyCSS: true,
      minifyJS: true
    }
  }
}

IV. Vuex & nuxtServerInit - SSR Essentials

To understand the best practice in this context, let's use the URL https://www.flagtick.com/snippet/javascript/convert-timestamp-to-iso-date-string/ as a reference. In reality, our URL structure for generating pages will follow this format:

pages/
└── snippet/
    └── _aspect/
        └── _code.vue

Firstly, let us double-check that the URL is registered in slugs.json before generating the static page for this URL from the Nuxt server.

[
   ...
   /snippet/javascript/convert-timestamp-to-iso-date-string/,
]

Next, let us sketch diagram outlining the process for loading pages under SSR and synchronizing with CSR from Nuxt components for user interactions such as clicks, etc.

Meta Data API: Create the script dynamically generates and sets the '<head>' properties for each page in the Nuxt.js application, providing flexibility in managing metadata and improving SEO.

» v-head.js

import axios from 'axios';

export default async ({ app, store, route, error, redirect }) => {
  
  let fullUrl = process.server
    ? process.env.BASE_URL + route.path
    : window.location.href;

  if (!fullUrl.endsWith('/')) {
    fullUrl = fullUrl + '/';
  }

  let title = '';
  let meta = [];
  let link = [];

  const convertedPath = (route.path
    .replace(/\//g, '_')
    .replace(/^_+|_+$/g, '')) + '.json';
    
  try {
    const response = await axios.get(`${metadataAPIEndpoint}`);
    const jsonData = response.data;
    
    if (undefined !== jsonData) {
      title = jsonData.title;
      // Add metadata based on JSON data
      meta = jsonData.meta;
      link = jsonData.link;
    }
  } catch (error) {
    // Handle error if metadata fetching fails
    title = 'The Innovative Blog for Programmers';
    meta = [
      { charset: 'utf-8' },
      // Add default metadata
      // ...
    ];
    link = [
      { rel: 'canonical', href: fullUrl }
      // Add default link
      // ...
    ];
  }
  
  // Commit metadata to Vuex store
  store.commit('setHeadProperties', { title, meta, link });
};

Snippet Detail API: Leveraging the nuxtServerInit method ensures that the Vuex store is properly initialized with necessary data before rendering the page, enabling server-side rendering (SSR) with the required initial state.

» stores/index.js

async nuxtServerInit({ commit, dispatch }, { app, route }) {
	app.$cookiz.set('locale', 'en');
	const { code } = route.params;
	await serverInitHandler.handleSnippet(dispatch, code);
},
...
async getSnippetFromServer({ commit }, { slug, lang, model }) {
  let { data } = await api.get('/api/front/snippet/'+slug, {
    params: {
      'lang': lang,
      'model': model
    }
  });
  commit('setSnippetDetail', data);
  return data;
},
...

Note: The getSnippetFromServer method asynchronously fetches snippet data from the server based on specified parameters, commits the data to the store, and returns it for further use in the application.

» stores/serverInitHandler.js

export async function handleSnippet(dispatch, code) {
  if (code) {
    await dispatch('getSnippetFromServer', {
      slug: code,
      lang: 'en',
      model: 'front'
    });
  }
}

The structure of _code.vue reflects the implementation of SSR for rendering this page. Key components include head, data, asyncData, computed, watch, and methods.

» pages/snippet/_aspect/_code.vue

<template>
  <div class="container">
    <div class="row height d-flex justify-content-center align-items-center">
      <div class="col-md-12">
        <div class="mt-2">
          <div class="space"></div>
          <div class="d-flex flex-row p-3">
            <img v-bind:src="snippet.user.avatar" alt="avatar" width="40" height="40" class="rounded-circle mr-3">
            ...
          </div>
          <div class="space"></div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  head() {
    return {
      title: this.$store.state.headProperties.title,
      meta: this.$store.state.headProperties.meta,
      link: this.$store.state.headProperties.link,
    };
  },
  data() {
    return {
      page: 1,
      snippet: {},
      locale: this.$cookiz.get('locale'),
    };
  },
  async asyncData({ params, store }) {
    await store.dispatch('getSnippetFromServer', {
      'slug': params.code,
      'lang': 'en',
      'model': 'front'
    });
  },
  computed: {
    snippetDetail() {
      return this.$store.getters.getSnippetDetail;
    }
  },
  watch: {
    snippetDetail: {
      handler(newVal) {
        if (newVal) {
          this.loadSnippet(newVal);
        }
      },
      immediate: true
    }
  },
  methods: {
    loadSnippet(item) {	
      let scope = this;
      if (item) {
        scope.snippet = item;
      }
    }
  }
};
</script>
<style scoped>
	<!-- Your CSS for this component -->
</style>

Note: If any executions in the Nuxt component relate to Vue lifecycle hooks like `created()` or `mounted()`, it is advisable to place them within the `process.client` block to prevent SSR rendering from the server.

V. Optimizing Vue Lifecycle Hooks for SEO

In Nuxt.js, asyncData is method used to fetch data asynchronously before rendering a page. When a user requests a page, Nuxt generates an initial HTML file (often referred to as a static file) that includes this pre-fetched data. As the page loads in the browser, Nuxt hydrates it by synchronizing the server-rendered content with client-side interactivity. This synchronization ensures that the page loads only once and that the content remains consistent between the initial server-rendered version and the hydrated version on the client side.

Hence, If Nuxt page loads content twice, it is likely due to errors in the asyncData method, impacting the hydration process.

Certainly! In Nuxt applications, the event system is vital for communication between nested components. Initializing a bus in the created() hook allows you to trigger events seamlessly across components.

» pages/snippet/_aspect/_code.vue

...
created() {
  if (process.client) {
    bus.$emit(Constant.HANDLE_NAV_BAR, false);
  }
}

As you can see here, we have enclosed it within `process.client` due to the invocation of the created() hook in `Header.vue`.

» layouts/partials/Header.vue

  ...
  created() {
    let scope = this;
    bus.$on(Constant.HANDLE_NAV_BAR, function (value) {
        scope.navMenu = !value;
    });
  }

Assuming you are using window.location.href to redirect users to specific pages, it is important to note that directly accessing window can disrupt server-side rendering. To avoid this, use process.client to ensure it's executed only on the client side.

  ...  
  <div>
    <span v-for="relative in snippet.relative" @click.prevent="jumpintoSnippet(relative)"
        class="snippet_name badge badge-dark mr-2" :key="relative.id">
      {{ relative }}
    </span>
  </div>
  ...
  jumpintoSnippet(relative) {
    if (process.client) {
      let aspect = generateSlug(this.snippetSSR.aspect.name);
      const url = '/snippet/' + aspect + '/' + convertToSlug(relative) + '/';
      window.location.href = url;
    }
  }

Additionally, using CSS animations to load HTML elements can result in content being loaded twice. For instance, consider the following animation:

<style scoped>
.animation_fade {
  animation: fadein 2s;
  -moz-animation: fadein 2s; /* Firefox */
  -webkit-animation: fadein 2s; /* Safari and Chrome */
  -o-animation: fadein 2s; /* Opera */
}
@keyframes fadein {
  from {
    opacity:0;
  }
  to {
    opacity:1;
  }
}
@-moz-keyframes fadein { /* Firefox */
  from {
    opacity:0;
  }
  to {
    opacity:1;
  }
}
@-webkit-keyframes fadein { /* Safari and Chrome */
  from {
    opacity:0;
  }
  to {
    opacity:1;
  }
}
@-o-keyframes fadein { /* Opera */
  from {
    opacity:0;
  }
  to {
    opacity: 1;
  }
}
</style>

After observing the loading performance of the URL https://www.flagtick.com/snippet/javascript/convert-timestamp-to-iso-date-string/, you may notice that it returns a 304 status code, indicating that the page is being served from the dist folder. This significantly accelerates loading times for the client-side, resulting in a better user experience.

VI. Conclusion

This guide has provided an overview of building server-side rendered Vue.js applications using Nuxt.js. We explored the project structure, set up Nuxt.js in an example application, delved into the Nuxt configuration file, and discussed essential SSR concepts such as Vuex and nuxtServerInit. If you encounter any problems or have questions while referring to our post, please don't hesitate to leave a comment below. We're here to support you.

Vuong Nguyen
Vuong Nguyen The individual is sociable and enjoys making friends, often sharing knowledge across various fields. |1 second ago
Vuong Nguyen The individual is sociable and enjoys making friends, often sharing knowledge across various fields. 1 second ago
You need to login to do this manipulation!