In traditional client-side rendering (CSR) applications, the browser fetches a minimal HTML file containing JavaScript code responsible for rendering the entire web page dynamically on the client-side. While CSR offers interactivity and flexibility, it has limitations in terms of initial page load time and SEO.Since SSR addresses these limitations by rendering the web page on the server before sending it to the client’s browser, it has become an essential technique for modern web development by offering significant advantages in terms of performance and search engine optimization (SEO).
Vue.js and Nuxt.js
Vue.js is a progressive JavaScript framework revered for its simplicity, flexibility, and reactivity, making it a prime choice for modern web applications. It boasts a component-based architecture, reactive data binding, and an intuitive API, empowering developers to effortlessly craft dynamic and interactive user interfaces.
Nuxt.js, built atop Vue.js, streamlines the development of Server-Side Rendering (SSR) applications. By abstracting complex SSR configurations, Nuxt.js simplifies the setup with conventions and automates tasks such as routing, code splitting, and static file serving.
Vue.js and Nuxt.js seamlessly complement each other for SSR. Vue.js provides the core functionality for dynamic UIs, while Nuxt.js enables server-side rendering of Vue.js components, ensuring fast initial page loads and optimal SEO. Nuxt.js simplifies SSR application development, allowing developers to focus on crafting exceptional user experiences without getting bogged down by SSR intricacies.
Key Features of Nuxt.js for SSR
Nuxt.js provides built-in support to implement SSR in Vue.js applications. Nuxt.js abstracts away the complexities of SSR implementation by automating server-side rendering and providing developers with a convention-based setup.
Automatic Server-Side Rendering: Nuxt.js handles server-side rendering automatically, eliminating the need for manual configuration. Developers can focus on building Vue.js components without worrying about SSR implementation details.
Routing and Code Splitting: Nuxt.js provides a built-in router and automatic code splitting, enabling efficient navigation and optimized performance for SSR applications.
Static File Serving: Nuxt.js serves static files, such as CSS, JavaScript, and images, directly from the server, improving performance and reducing load times.
Node.js Version Compatibility
Different versions of Vue.js and Nuxt.js may demand specific versions of Node.js to in order to function optimally. Hence it’s imperative to ensure compatibility with the correct Node.js version when working with Vue.js and Nuxt.js, .
Execute the following command on your terminal to find out the installed Node.js version on your system:
node -v
Ensure that the installed Node.js version meets or exceeds the recommended version specified by Vue.js and Nuxt.js.
Creating a New Nuxt.js Project
Nuxt.js provides a CLI (Command Line Interface) tool for scaffolding new projects called create-nuxt-app. This tool automates the setup process and generates a ready-to-use Nuxt.js project with predefined configurations.
Run the following command to install create-nuxt-app globally on your system:
npm install -g create-nuxt-app
Once create-nuxt-app is installed, run the following command to create your new Nuxt.js project:
create-nuxt-app ssr-with-vue-js-nuxt-js
You will be asked to choose a range of presets for your project, including options for server frameworks, UI frameworks, testing frameworks, and more. Feel free to select the options that best suit your project requirements or choose the default preset for a basic setup.
Once the setup is complete, navigate to your project directory.
cd ssr-with-vue-js-nuxt-js
We now have a project structure that will serve as a basis for building a basic level Nuxt.js application.
ssr-with-vue-js-nuxt-js ├── README.md ├── components │ ├── NuxtLogo.vue │ ├── Tutorial.vue ├── pages │ ├── index.vue ├── static │ └── favicon.ico ├── store │ └── README.md ├── .editorconfig ├── .gitignore ├── .nuxtignore ├── nuxt.config.js ├── package.json └── yarn.lock
Let’s have a brief overview of the created project structure:
- components: Components directory contains Vue.js components that are reusable across different pages.
- pages: Points to the location for application’s views or pages. Each .vue file responds to a route in your application.
- static: All the static project files should be place in this directory, and each asset will be served under the same URL path, /.
- store: This is where you’ll find all the Vuex store files. Vuex serves as a state management pattern and library tailor made for Vue.js applications.
- nuxt.config.js: Contains the configuration options for Nuxt.js.
Next, run the following command to start the development server:
npm run dev
This command will start the Nuxt.js development server, allowing you to preview your project in the browser at http://localhost:3000.
Preview your project in the browser at http://localhost:3000
Customizing SSR Behavior
The nuxt.config.js configuration file allows us to define various settings and options related to SSR.
// nuxt.config.js export default { // Global page headers: https://go.nuxtjs.dev/config-head head: { title: 'Server-Side Rendering with Vue.js and Nuxt.js', htmlAttrs: { lang: 'en' }, meta: [ { charset: 'utf-8' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' }, { hid: 'description', name: 'description', content: '' }, { name: 'format-detection', content: 'telephone=no' } ], link: [ { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' } ] }, // Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins plugins: [], // Auto import components: https://go.nuxtjs.dev/config-components components: true, // Build Configuration: https://go.nuxtjs.dev/config-build build: {} }
Here is a brief insight into some of the key default configurations available in your Nuxt.js project:
- Global Page Headers: Define global meta tags and other head elements for server-rendered pages. This includes settings such as the title, language, and meta tags.
- Plugins: Register plugins to run on the server-side during SSR initialization. This can include custom plugins for tasks such as data fetching or authentication.
- Auto Import Components: Configure whether Nuxt.js should automatically import components. When set to true, components are automatically imported based on the file system structure.
- Build Configuration: Customize the webpack build configuration for SSR optimization. This includes settings related to code splitting, bundle size optimization, and performance tweaks.
While Nuxt.js automates SSR implementation, developers can further customize SSR behavior according to specific project requirements using the nuxt.config.js file.
Setting up Axios for Nuxt.js
Axios is a widely-used HTTP client for making requests from web browsers and Node.js environments. In Nuxt.js, Axios is used as a standard for fetching data from APIs, making it an essential tool for building dynamic web applications.
However, to use Axios in your Nuxt.js project, we not only need the actual Axios library that provides the functionality for making HTTP requests, but we will also need @nuxtjs/axios; a Nuxt.js module that integrates Axios seamlessly into a Nuxt.js project, and provides additional features and configurations tailored specifically for Nuxt.js applications.
To install both packages, run the following commands in your terminal:
npm install axios npm install @nuxtjs/axios
You might wonder why it’s necessary to also install @nuxtjs/axios. That is because the Nuxt module, @nuxtjs/axios, offers built-in features such as automatic serialization of JavaScript Date objects, support for loading indicators during requests, and integration with Nuxt.js features like server middleware and Vuex store.
We also need to configure Nuxt.js to use Axios. Open nuxt.config.js file and add the following code:
// nuxt.config.js // Nuxt.js modules modules: [ // Axios module '@nuxtjs/axios', ], // Axios module configuration axios: { // API endpoint baseURL: 'http://localhost:3000' // Assuming API server is running locally }, // Build Configuration: https://go.nuxtjs.dev/config-build build: { transpile: [({ isLegacy }) => isLegacy && 'axios'] }
Adding these configuration ensures that Axios is integrated into your Nuxt.js application, and works with features such as server middleware and Vuex store. Additionally, the transpile option also makes sure that Axios is properly transpiled for compatibility with older browsers if needed.
Building a Product Listing Page
With our project baseline in place, we are now set to showcase an effective Server-Side Rendering (SSR) implementation with Vue.js and Nuxt.js.
We will start by adding a new page to our project and use it to render a dynamic product listing fetched from an Axios API, which retrieves data from a JSON file stored in the static assets section.
Inside the pages folder, create a new products.vue file and add the following code:
// pages/products.vue <template> <div class="product-list"> <h1>Product Catalog</h1> <div v-for="product in displayedProducts" :key="product.id" class="product"> <img :src="product.image" :alt="product.name" /> <div class="product-details"> <h2>{{ product.name }}</h2> <p>{{ product.description }}</p> <p>Price: ${{ product.price }}</p> <router-link :to="{ name: 'product-details', params: { id: product.id }}"> View Details </router-link> </div> </div> <pagination :total="totalProducts" @pageChange="fetchProducts" /> </div> </template> <script> export default { data() { return { products: [], totalProducts: 0, currentPage: 1, productsPerPage: 3 // Adjust the number of products per page as needed }; }, mounted() { this.fetchProducts(); }, computed: { displayedProducts() { const startIndex = (this.currentPage - 1) * this.productsPerPage; const endIndex = startIndex + this.productsPerPage; return this.products.slice(startIndex, endIndex); } }, methods: { async fetchProducts(page = 1) { try { const response = await this.$axios.get('/api/products', { params: { page, limit: this.productsPerPage } }); this.products = response.data.products; this.totalProducts = response.data.total; this.currentPage = page; // Update current page } catch (error) { console.error('Error fetching products:', error); } } } }; </script>
This Vue page utilizes Axios to fetch data from the /api/products endpoint, and displays a list of products, featuring attributes such as name, description, price, and an image.
By adjusting the productsPerPage variable, you can also control the number of products displayed per page.
Adding a Pagination Component
In addition to the product listing, we want to enhance user experience by implementing pagination through multiple pages of product listings.
Let’s add a new Pagination.vue file inside the components folder. We will use this component to handle the pagination controls and logic.
// components/Pagination.vue <template> <div class="pagination"> <button @click="prevPage" :disabled="currentPage === 1">Previous</button> <span>Page {{ currentPage }} of {{ totalPages }}</span> <button @click="nextPage" :disabled="currentPage === totalPages">Next</button> </div> </template> <script> export default { props: { total: { type: Number, required: true } }, data() { return { currentPage: 1 }; }, computed: { totalPages() { return Math.ceil(this.total / 3); // Assuming 10 products per page } }, methods: { prevPage() { if (this.currentPage > 1) { this.currentPage--; this.$emit('pageChange', this.currentPage); // Emit the current page number } }, nextPage() { if (this.currentPage < this.totalPages) { this.currentPage++; this.$emit('pageChange', this.currentPage); // Emit the current page number } } } }; </script>
The component consists of three main parts: buttons for navigating to the previous and next pages, a span displaying the current page number and total number of pages, and the logic to handle pagination.
The Previous and Next buttons will allow us to navigate through pages, with disabled state handling when the user is on the first or last page.
The component also emits the current page number whenever it changes, allowing the parent component, products.vue, to react accordingly and fetch the corresponding data.
Fetching Product Data
To handle fetching product data from the backend API, the application utilizes the Axios module. You can create a new plugin file named axios.js inside the plugins folder with the following code:
// plugins/axios.js export default function ({ $axios }) { // Set baseURL (optional, if not set in axios module configuration) $axios.setBaseURL('https://localhost:3000'); // Set headers (optional) $axios.setHeader('Content-Type', 'application/json'); // Add request interceptor $axios.onRequest(config => { console.log('Making request to ' + config.url); }); // Add response interceptor $axios.onResponse(response => { console.log('Response received from ' + response.config.url); }); // Add error interceptor $axios.onError(error => { console.error('Error response:', error.response); }); }
The axios plugin is responsible for configuring Axios requests, setting the base URL for all Axios requests to the local API server running on port 3000, and ensuring that all the incoming requests include the necessary headers for handling JSON data. It also intercepts requests, responses, and errors to provide insight into the request lifecycle.
For handling product data retrieval, we need to create a new API handler file named products.js inside the api folder with the following code:
// /api/products.js // Import products data const productsData = require('../static/products.json'); // Define a handler for /api/products route export default function(req, res) { try { // Set the response header res.setHeader('Content-Type', 'application/json'); // Extract pagination parameters from the query string const page = req.query && req.query.page ? parseInt(req.query.page) : 1; // Default to page 1 if not provided const limit = req.query && req.query.limit ? parseInt(req.query.limit) : 10; // Default limit to 10 if not provided // Calculate start and end indices for pagination const startIndex = (page - 1) * limit; const endIndex = startIndex + limit; // Slice the products array to get the subset for the current page const paginatedProducts = productsData.products.slice(startIndex, endIndex); // Construct the response object with paginated products and total count const response = { products: paginatedProducts, total: productsData.total }; // Send the JSON response res.statusCode = 200; // Set the status code res.end(JSON.stringify(response)); // Send the JSON response } catch (error) { // Handle errors console.error('Error serving products:', error); res.statusCode = 500; // Set the status code res.end(JSON.stringify({ error: 'Internal Server Error' })); // Send the error response } }
The handler processes requests to the /api/products route, extracts pagination parameters from the query string, calculates the appropriate subset of products for the requested page, and constructs the response object containing the paginated products and total count.
Additionally, to set up the product data for the application, create a new file named products.json inside the static folder with the following content:
// static/products.json { "products": [ { "id": 1, "name": "Product 1", "description": "Description of Product 1", "price": 10.99, "image": "https://via.placeholder.com/150" }, { "id": 2, "name": "Product 2", "description": "Description of Product 2", "price": 19.99, "image": "https://via.placeholder.com/150" }, { "id": 3, "name": "Product 3", "description": "Description of Product 3", "price": 15.99, "image": "https://via.placeholder.com/150" }, { "id": 4, "name": "Product 4", "description": "Description of Product 4", "price": 12.49, "image": "https://via.placeholder.com/150" }, { "id": 5, "name": "Product 5", "description": "Description of Product 5", "price": 8.99, "image": "https://via.placeholder.com/150" }, { "id": 6, "name": "Product 6", "description": "Description of Product 6", "price": 22.99, "image": "https://via.placeholder.com/150" }, { "id": 7, "name": "Product 7", "description": "Description of Product 7", "price": 14.99, "image": "https://via.placeholder.com/150" }, { "id": 8, "name": "Product 8", "description": "Description of Product 8", "price": 9.99, "image": "https://via.placeholder.com/150" }, { "id": 9, "name": "Product 9", "description": "Description of Product 9", "price": 17.99, "image": "https://via.placeholder.com/150" }, { "id": 10, "name": "Product 10", "description": "Description of Product 10", "price": 11.99, "image": "https://via.placeholder.com/150" } ], "total": 10 }
This file contains sample product data in JSON format, including details such as product names, descriptions, prices, and images.
Even though we are using a static JSON file but, with minor adjustments to the example code base, this functionality can be extended to work seamlessly with various popular relational or non-relational databases.
Finally, to integrate these configurations into the Nuxt.js project, update the nuxt.config.js file with the following changes:
// nuxt.config.js serverMiddleware: [ // Register the /api/products route { path: '/api/products', handler: '~/api/products.js' } ], plugins: [ { src: '~/plugins/axios.js' } // Include Axios plugin ]
By including these configurations, we ensure that the Axios plugin is utilized for handling HTTP requests, and the serverMiddleware registers the /api/products route, enabling the application to fetch product data seamlessly.
Your final nuxt.config.js code should look like this:
// nuxt.config.js export default { // Global page headers: https://go.nuxtjs.dev/config-head head: { title: 'Server-Side Rendering with Vue.js and Nuxt.js', htmlAttrs: { lang: 'en' }, meta: [ { charset: 'utf-8' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' }, { hid: 'description', name: 'description', content: '' }, { name: 'format-detection', content: 'telephone=no' } ], link: [ { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' } ] }, serverMiddleware: [ // Register the /api/products route { path: '/api/products', handler: '~/api/products.js' } ], // Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins plugins: [ { src: '~/plugins/axios.js' } // Include Axios plugin ], // Auto import components: https://go.nuxtjs.dev/config-components components: true, // Nuxt.js modules modules: [ // Axios module '@nuxtjs/axios', ], // Axios module configuration axios: { // API endpoint baseURL: 'http://localhost:3000' // Assuming API server is running locally }, // Build Configuration: https://go.nuxtjs.dev/config-build build: { transpile: [({ isLegacy }) => isLegacy && 'axios'] } }
Now we are all ready to preview the products listing page in the browser at http://localhost:3000/products
Feel free to use the pagination controls to navigate through product listings and observe Server-Side Rendering in action.
Conclusion:
In this article, we explored key features of Server-Side Rendering (SSR) with Vue.js and Nuxt.js, customized SSR behavior using nuxt.config.js, and integrated Axios for seamless data fetching. We also built a dynamic product listing page with pagination, fetching data from a static JSON file. Understanding SSR with Vue.js and Nuxt.js equips developers with the tools to efficiently craft high-performance, SEO-friendly web applications.