Skip to content

How to Create and Deploy a Vue Component Library to NPM

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

Introduction

When working across multiple Vue projects that share the same design system, it is more efficient and faster to have a component library that you can reference for all your components within the different projects. In this article, we will go through the steps needed to create and deploy a Vue component library to npm, so we can reuse them across various projects.

  • Create a vue component library
  • Register library components
  • Setup a build process
  • Test locally then publish our library to npm

Create a Vue component library

Setup our Project

Let’s start by creating our Vue project. We would be using yarn for package management.

To start, lets run

npm init

For this tutorial, we are going to create just a single component within our component library. One of the things to consider when building a component library at scale is to allow import of just single components to enable tree shaking.

We would be using this folder structure for our component library:

- src /
  - components /
    - button /
      - button.vue
      - index.ts

    - index.ts

  - styles /
    - components /
        - _button.scss

    - index.scss

- Package.json
- rollup.config.js

Create button component

Let's create a simple button component. We define the basic props our component can accept and compute the class based on the props.

Add this to the button.vue file.

<!-- src/components/button/button.vue -->
<template>
    <button
        v-bind="$attrs"
        :class="rootClasses"
        :type="type"
        :disabled="computedDisabled"
    >
      <slot></slot>
    </button>
</template>

<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
    name: 'DSButton',
    inheritAttrs: false,
    props: {
        /**
         * disabled status
         * @values true, false
         */
        disabled: {
            type: Boolean,
        },
        /**
        * Color of button
        * @values primary, secondary
        */
        variant: {
            type: String,
            validator: (value: string) => {
                return [
                    'primary',
                    'secondary'
                ].indexOf(value) >= 0
            }
        },
        /**
         * type of button
         * @values button, submit
         */
        type: {
            type: String,
            default: 'button',
            validator: (value: string) => {
                return [
                    'button',
                    'submit',
                    'reset'
                ].indexOf(value) >= 0
            }
        },
        /**
         * Size of button
         * @values sm, md, lg
         */
        size: {
            type: String,
            validator: (value: string) => {
                return [
                    'sm',
                    'md',
                    'lg'
                ].indexOf(value) >= 0
            }
        }
    },
    computed: {
        rootClasses() {
            return [
                'ds-button',
                'ds-button--' + this.size,
                'ds-button--' + this.variant
            ]
        },
        computedDisabled() {
            if (this.disabled) return true
            return null
        }
    }
})
</script>

Let's style the button component, added our variant and size classes based on what is specified in the button component file.

Add this to the _button.scss file.

// src/styles/components/_button.scss
$primary: '#0e34cd';
$secondary: '#b9b9b9';
$white: '#ffffff';
$black: '#000000';
$small: '.75rem';
$medium: '1.25rem';
$large: '1.5rem';

.ds-button {
    position: relative;
    display: inline-flex;
    cursor: pointer;
    text-align: center;
    white-space: nowrap;
    align-items: center;
    justify-content: center;
    vertical-align: top;
    text-decoration: none;
    outline: none;

    // variant
    &--primary {
        background-color: $primary;
        color: $white;
    }
    &--secondary {
        background-color: $secondary;
        color: $black;
    }

    // size
    &--sm {
        min-width: $small;
    }
    &--md {
        min-width: $medium;
    }
    &--lg {
        min-width: $large;
    }
}

Register library components

Next we need to register our components. To do this, we import all our components into a single file and create our install method.

// src/components/button/index.ts This file exports the install methods as default. For cases where we need to import just this button component in our other projects, it also exports Button which we will use in a bit.

// src/components/button/index.ts
import { App, Plugin } from 'vue'

import Button from './button.vue'

export default {
    install(Vue: App) {
        Vue.component(Button.name, Button)
    }
} as Plugin

export {
    Button as DSButton
}

// src/components/index.ts Lets import all the components in our components folder here. Since we have only our button component, we will imort it.

// src/components/index.ts
import Button from './button'

export {
    Button
}

// src/styles/index.scss Lets import all the components styles into index.scss in our styles folder. This helps us with having one source of export for all of our styles.

// src/styles/index.scss
@import "components/_button";

// src/index.ts Lets import all the components into index.ts in our src folder. Here, we create our install method for all the component. We export DSLibrary as default, and also export all our components.

// src/index.ts
import { App } from 'vue'

import * as components from './components'

const DSLibrary = {
    install(app: App) {
        // Auto import all components
        for (const componentKey in components) {
            app.use((components as any)[componentKey])
        }
    }
}

export default DSLibrary

// export all components as vue plugin
export * from './components'

Lets create a file called shim-vue.d.ts to help us with importing Vue files into our TypeScript files, and remove any linting errors caused by it.

// src/shim-vue.d.ts
declare module '*.vue' {
  import type { DefineComponent } from 'vue';
  const component: DefineComponent<{}, {}, any>;
  export default component;
}

Setup a build process

In building the library, we need to create a bundled and minidifed of the library that will be shared to npm, for this we would use Rollup. Rollup is a module bundler for JavaScript which compiles small pieces of code into something larger.

  • build component (vue code)
  • build styling (scss code)

Install rollup, and all of the rollup modules we would need.

$ npm i -D rollup rollup-plugin-vue rollup-plugin-terser rollup-plugin-typescript2 @rollup/plugin-babel @rollup/plugin-commonjs @rollup/plugin-node-resolve

Build component (vue code)

we need two major build types for our component library to support various projects.

  • ES module - It has no requirements or exports
  • CommonJS module

Lets create a rollup.config.js file in the root folder and paste this there.

import { text } from './build/banner.json'
import packageInfo from './package.json'

import vue from 'rollup-plugin-vue'
import node from '@rollup/plugin-node-resolve'
import cjs from '@rollup/plugin-commonjs'
import babel from '@rollup/plugin-babel'
import { terser } from 'rollup-plugin-terser'
import typescript from 'rollup-plugin-typescript2';

import fs from 'fs'
import path from 'path'

const baseFolderPath = './src/components/'
const banner = text.replace('${version}', packageInfo.version)

const components = fs
    .readdirSync(baseFolderPath)
    .filter((f) =>
        fs.statSync(path.join(baseFolderPath, f)).isDirectory()
    )

const entries = {
    'index': './src/index.ts',
    ...components.reduce((obj, name) => {
        obj[name] = (baseFolderPath + name)
        return obj
    }, {})
}

const babelOptions = {
    babelHelpers: 'bundled'
}

const vuePluginConfig = {
    template: {
        isProduction: true,
        compilerOptions: {
            whitespace: 'condense'
        }
    }
}

const capitalize = (s) => {
    if (typeof s !== 'string') return ''
    return s.charAt(0).toUpperCase() + s.slice(1)
}

export default () => {
    let config = []

    if (process.env.MINIFY === 'true') {
        config = config.filter((c) => !!c.output.file)
        config.forEach((c) => {
            c.output.file = c.output.file.replace(/\.m?js/g, r => `.min${r}`)
            c.plugins.push(terser({
                output: {
                    comments: '/^!/'
                }
            }))
        })
    }
    return config
}

Next, we create a build folder in the root of our project, and add a file called banner.json to it. We want our builds to contain the current app version everytime we build. This file is already imported into the rollup.config.js file, and we use the package version from our package.json to update the version.

{
    "text": "/*! DS Library v${version} */\n"
}

Currently, our config is an empty array. Next, we will add the different builds we want.

entries: path to files we want rollup to bundle external: the external package needed output.format: the bundled file format output.dir: the bundled files directory output.banner: text added to the begining of the file plugins: specify methods used to customize rollup behaviour

  • First we create an esm build for each component in our library:

        config = [{
             input: entries,
             external: ['vue'],
             output: {
                 format: 'esm',
                 dir: `dist/esm`,
                 entryFileNames: '[name].mjs',
                 chunkFileNames: '[name]-[hash].mjs',
             },
             plugins: [
                 node({
                     extensions: ['.vue', '.ts']
                 }),
                 typescript({
                     typescript: require('typescript')
                 }),
                 vue(vuePluginConfig),
                 babel(babelOptions),
                 cjs()
             ]
         }],
    
  • Next we create a single esm build for all the components:

        config = [
          ...,
         {
             input: 'src/index.ts',
             external: ['vue'],
             output: {
                 format: 'esm',
                 file: 'dist/ds-library.mjs',
                 banner: banner
             },
             plugins: [
                 node({
                     extensions: ['.vue', '.ts']
                 }),
                 typescript({
                     typescript: require('typescript')
                 }),
                 vue(vuePluginConfig),
                 babel(babelOptions),
                 cjs()
             ]
         }
       ],
    
  • Then we create an cjs build for each component in our library:

        config = [
          ...,
          ...,
         {
             input: entries,
             external: ['vue'],
             output: {
                 format: 'cjs',
                 dir: 'dist/cjs',
                 exports: 'named'
             },
             plugins: [
                 node({
                     extensions: ['.vue', '.ts']
                 }),
                 typescript({
                     typescript: require('typescript')
                 }),
                 vue(vuePluginConfig),
                 babel(babelOptions),
                 cjs()
             ]
         }
       ],
    
  • After that, we create a single cjs build for each component in our library

        config = [
          ...,
          ...,
          ...,
         {
             input: 'src/index.ts',
             external: ['vue'],
             output: {
                 format: 'umd',
                 name: capitalize('ds-library'),
                 file: 'dist/ds-library.js',
                 exports: 'named',
                 banner: banner,
                 globals: {
                     vue: 'Vue'
                 }
             },
             plugins: [
                 node({
                     extensions: ['.vue', '.ts']
                 }),
                 typescript({
                     typescript: require('typescript')
                 }),
                 vue(vuePluginConfig),
                 babel(babelOptions),
                 cjs()
             ]
         }
    
       ],
    

Finally, we update our package.json with our script command. We need both rimraf (to remove our old dist folder before we create a new bundle) and clean-css (to minify our bundled css file), so lets install:

$ npm i -D rimraf clean-css-cli

now lets update our package.json script

build:vue = rollup and minify build:style = bundled our style from scss to css, add our banner text(version number like we did above) and save in dist/ds-library.css then create a minified version.

For the banner text in our css file, we need to create a print-banner.js file inside the build folder. It takes our banner text, and writes it to the file.


// build/print-banner.js
const packageInfo = require('../package.json')
const { text } = require('./banner.json')

process.stdout.write(text.replace('${version}', packageInfo.version))
process.stdin.pipe(process.stdout)

build:lib = delete dist folder, build vue and style code publish:lib = run our build lib command then publish to npm

    "scripts": {
        "build:vue": "rollup -c && rollup -c --environment MINIFY",
        "build:vue:watch": "rollup -c --watch",
        "build:style": "sass --no-charset ./src/styles/index.scss | node ./build/print-banner.js > dist/ds-library.css && cleancss -o dist/ds-library.min.css dist/ds-library.css",
        "build:lib": "rimraf dist && npm run build:vue && npm run build:style",
        "publish:lib": "npm run build:lib && npm publish"
    },
    "peerDependencies": {
        "vue": "^3.0.0"
    },

Test locally then publish our library to npm

Now that we are done, we can test locally by running npm link in the root directory of this repo and also running npm link "package name" in the root directory of our test project. After this, the package will be available for use in our test package.

Once tested, you can run npm publish:lib to build and deploy to npm.

If you have any questions or run into any trouble, feel free to reach out on Twitter or Github.

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

You might also like

Understanding Vue's Reactive Data cover image

Understanding Vue's Reactive Data

Introduction Web development has always been about creating dynamic experiences. One of the biggest challenges developers face is managing how data changes over time and reflecting these changes in the UI promptly and accurately. This is where Vue.js, one of the most popular JavaScript frameworks, excels with its powerful reactive data system. In this article, we dig into the heart of Vue's reactivity system. We unravel how it perfectly syncs your application UI with the underlying data state, allowing for a seamless user experience. Whether new to Vue or looking to deepen your understanding, this guide will provide a clear and concise overview of Vue's reactivity, empowering you to build more efficient and responsive Vue 3 applications. So, let’s kick off and embark on this journey to decode Vue's reactive data system. What is Vue's Reactive Data? What does it mean for data to be ”'reactive”? In essence, when data is reactive, it means that every time the data changes, all parts of the UI that rely on this data automatically update to reflect these changes. This ensures that the user is always looking at the most current state of the application. At its core, Vue's Reactive Data is like a superpower for your application data. Think of it like a mirror - whatever changes you make in your data, the user interface (UI) reflects these changes instantly, like a mirror reflecting your image. This automatic update feature is what we refer to as “reactivity”. To visualize this concept, let's use an example of a simple Vue application displaying a message on the screen: ` In this application, 'message' is a piece of data that says 'Hello Vue!'. Let's say you change this message to 'Goodbye Vue!' later in your code, like when a button is clicked. ` With Vue's reactivity, when you change your data, the UI automatically updates to 'Goodbye Vue!' instead of 'Hello Vue!'. You don't have to write extra code to make this update happen - Vue's Reactive Data system takes care of it. How does it work? Let's keep the mirror example going. Vue's Reactive Data is the mirror that reflects your data changes in the UI. But how does this mirror know when and what to reflect? That's where Vue's underlying mechanism comes into play. Vue has a behind-the-scenes mechanism that helps it stay alerted to any changes in your data. When you create a reactive data object, Vue doesn't just leave it as it is. Instead, it sends this data object through a transformation process and wraps it up in a Proxy. Proxy objects are powerful and can detect when a property is changed, updated, or deleted. Let's use our previous example: ` Consider our “message” data as a book in a library. Vue places this book (our data) within a special book cover (the Proxy). This book cover is unique - it's embedded with a tracking device that notifies Vue every time someone reads the book (accesses the data) or annotates a page (changes the data). In our example, the reactive function creates a Proxy object that wraps around our state object. When you change the 'message': ` The Proxy notices this (like a built-in alarm going off) and alerts Vue that something has changed. Vue then updates the UI to reflect this change. Let’s look deeper into what Vue is doing for us and how it transforms our object into a Proxy object. You don't have to worry about creating or managing the Proxy; Vue handles everything. ` In the example above, we encapsulate our object, in this case, “state”, converting it into a Proxy object. Note that within the second argument of the Proxy, we have two methods: a getter and a setter. The getter method is straightforward: it merely returns the value, which in this instance is “state.message” equating to 'Hello Vue!' Meanwhile, the setter method comes into play when a new value is assigned, as in the case of “state.message = ‘Hey young padawan!’”. Here, “value” becomes our new 'Hey young padawan!', prompting the property to update. This action, in turn, triggers the reactivity system, which subsequently updates the DOM. Venturing Further into the Depths If you have been paying attention to our examples above, you might have noticed that inside the Proxy method, we call the functions track and trigger to run our reactivity. Let’s try to understand a bit more about them. You see, Vue 3 reactivity data is more about Proxy objects. Let’s create a new example: ` In this example, when you click on the button, the message's value changes. This change triggers the effect function to run, as it's actively listening for any changes in its dependencies. How does the effect property know when to be called? Vue 3 has three main functions to run our reactivity: effect, track, and trigger. The effect function is like our supervisor. It steps in and takes action when our data changes – similar to our effect method, we will dive in more later. Next, we have the track function. It notes down all the important data we need to keep an eye on. In our case, this data would be state.message. Lastly, we've got the trigger function. This one is like our alarm bell. It alerts the effect function whenever our important data (the stuff track is keeping an eye on) changes. In this way, trigger, track, and effect work together to keep our Vue application reacting smoothly to changes in data. Let’s go back to them: ` Tracking (Dependency Collection) Tracking is the process of registering dependencies between reactive objects and the effects that depend on them. When a reactive property is read, it's "tracked" as a dependency of the current running effect. When we execute track(), we essentially store our effects in a Set object. But what exactly is an "effect"? If we revisit our previous example, we see that the effect method must be run whenever any property changes. This action — running the effect method in response to property changes — is what we refer to as an "Effect"! (computed property, watcher, etc.) > Note: We'll outline a basic, high-level overview of what might happen under the hood. Please note that the actual implementation is more complex and optimized, but this should give you an idea of how it works. Let’s see how it works! In our example, we have the following reactive object: ` We need a way to reference the reactive object with its effects. For that, we use a WeakMap. Which type is going to look something like this: ` We are using a WeakMap to set our object state as the target (or key). In the Vue code, they call this object targetMap. Within this targetMap object, our value is an object named depMap of Map type. Here, the keys represent our properties (in our case, that would be message and showSword), and the values correspond to their effects – remember, they are stored in a Set that in Vue 3 we refer to as dep. Huh… It might seem a bit complex, right? Let's make it more straightforward with a visual example: With the above explained, let’s see what this Track method kind of looks like and how it uses this targetMap. This method essentially is doing something like this: ` At this point, you have to be wondering, how does Vue 3 know what activeEffect should run? Vue 3 keeps track of the currently running effect by using a global variable. When an effect is executed, Vue temporarily stores a reference to it in this global variable, allowing the track function to access the currently running effect and associate it with the accessed reactive property. This global variable is called inside Vue as activeEffect. Vue 3 knows which effect is assigned to this global variable by wrapping the effects functions in a method that invokes the effect whenever a dependency changes. And yes, you guessed, that method is our effect method. ` This method behind the scenes is doing something similar to this: ` The handling of activeEffect within Vue's reactivity system is a dance of careful timing, scoping, and context preservation. Let’s go step by step on how this is working all together. When we run our Effect method for the first time, we call the get trap of the Proxy. ` When running the get trap, we have our activeEffect so we can store it as a dependency. ` This coordination ensures that when a reactive property is accessed within an effect, the track function knows which effect is responsible for that access. Trigger Method Our last method makes this Reactive system to be complete. The trigger method looks up the dependencies for the given target and key and re-runs all dependent effects. ` Conclusion Diving into Vue 3's reactivity system has been like unlocking a hidden superpower in my web development toolkit, and honestly, I've had a blast learning about it. From the rudimentary elements of reactive data and instantaneous UI updates to the intricate details involving Proxies, track and trigger functions, and effects, Vue 3's reactivity is an impressively robust framework for building dynamic and responsive applications. In our journey through Vue 3's reactivity, we've uncovered how this framework ensures real-time and precise updates to the UI. We've delved into the use of Proxies to intercept and monitor variable changes and dissected the roles of track and trigger functions, along with the 'effect' method, in facilitating seamless UI updates. Along the way, we've also discovered how Vue ingeniously manages data dependencies through sophisticated data structures like WeakMaps and Sets, offering us a glimpse into its efficient approach to change detection and UI rendering. Whether you're just starting with Vue 3 or an experienced developer looking to level up, understanding this reactivity system is a game-changer. It doesn't just streamline the development process; it enables you to create more interactive, scalable, and maintainable applications. I love Vue 3, and mastering its reactivity system has been enlightening and fun. Thanks for reading, and as always, happy coding!...

Understanding Vue.js's <Suspense> and Async Components cover image

Understanding Vue.js's <Suspense> and Async Components

In this blog post, we will delve into how and async components work, their benefits, and practical implementation strategies to make your Vue.js applications more efficient and user-friendly. Without further ado, let’s get started! Suspense Let's kick off by explaining what Suspense components are. They are a new component that helps manage how your application handles components that need to await for some async resource to resolve, like fetching data from a server, waiting for images to load, or any other task that might take some time to complete before they can be properly rendered. Imagine you're building a web page that needs to load data from a server, and you have 2 components that fetch the data you need as they will show different things. Typically, you might see a loading spinner or a skeleton while the data is being fetched. Suspense components make it easier to handle these scenarios. Instead of manually managing loading states and error messages for each component that needs to fetch data, Suspense components let you wrap all these components together. Inside this wrapper, you can define: 1. What to show while the data is loading (like a loading spinner). 2. The actual content that should be displayed once the data is successfully fetched. This way, Vue Suspense simplifies the process of handling asynchronous operations (like data fetching) and improves the user (and the developer) experience by providing a more seamless and integrated way to show loading states and handle errors. There are two types of async dependencies that can wait on: - Components with an async setup() hook. This includes components using with top-level await expressions. *Note: These can only be used within a component.* - Async Components. Async components Vue's asynchronous components are like a smart loading system for your web app. Imagine your app as a big puzzle. Normally, you'd put together all the pieces at once, which can take time. But what if some pieces aren't needed right away? Asynchronous components help with this. Here's how they work: - Load Only What's Needed: Just like only picking up puzzle pieces you need right now, asynchronous components let your app load only the parts that are immediately necessary. Other parts can be loaded later, as needed. - Faster Start: Your app starts up faster because it doesn't have to load everything at once. It's like quickly starting with the border of a puzzle and filling in the rest later. - Save Resources: It uses your web resources (like internet data) more wisely, only grabbing what’s essential when it's essential. In short, asynchronous components make your app quicker to start and more efficient, improving the overall experience for your users. Example: ` Combining Async Components and Suspense Let's explore how combining asynchronous components with Vue's Suspense feature can enhance your application. When asynchronous components are used with Vue's Suspense, they form a powerful combination. The key point is that async components are "suspensable" by default. This means they can be easily integrated with Suspense to improve how your app handles loading and rendering components. When used together, you can do the following things: - Centralized Loading and Error Handling: With Suspense, you don't have to handle loading and error states individually for each async component. Instead, you can define a single loading indicator or error message within the Suspense component. This unified approach simplifies your code and ensures consistency across different parts of your app. - Flexible and Clean Code Structure: By combining async components with Suspense, your code becomes more organized and easier to maintain. An asynchronous component has the flexibility to operate independently of Suspense's oversight. By setting suspensible: false in its options, the component takes charge of its own loading behavior. This means that instead of relying on Suspense to manage when it appears, the component itself dictates its loading state and presentation. This option is particularly useful for components that have specific loading logic or visuals they need to maintain, separate from the broader Suspense-driven loading strategy in the application. In practice, this combo allows you to create a user interface that feels responsive and cohesive. Users see a well-timed loading indicator while the necessary components are being fetched, and if something goes wrong, a single, well-crafted error message is displayed. It's like ensuring that the entire puzzle is either revealed in its completed form or not at all rather than showing disjointed parts at different times. How it works When a component inside the boundary is waiting for something asynchronous, shows fallback content. This fallback content can be anything you choose, such as a loading spinner or a message indicating that data is being loaded. Example Usage Let’s use a simple example: In the visual example provided, imagine we have two Vue components: one showcasing a selected Pokémon, Eevee, and a carousel showcasing a variety of other Pokémon. Both components are designed to fetch data asynchronously. Without , while the data is being fetched, we would typically see two separate loading indicators: one for the Eevee Pokemon that is selected and another for the carousel. This can make the page look disjointed and be a less-than-ideal user experience. We could display a single, cohesive loading indicator by wrapping both components inside a boundary. This unified loading state would persist until all the data for both components—the single Pokémon display and the carousel—has been fetched and is ready to be rendered. Here's how you might structure the code for such a scenario: ` Here, is the component that's performing asynchronous operations. While loading, the text 'Loading...' is displayed to the user. Great! But what about when things don't go as planned and an error occurs? Currently, Vue's doesn't directly handle errors within its boundary. However, there's a neat workaround. You can use the onErrorCaptured() hook in the parent component of to catch and manage errors. Here's how it works: ` If we run this code, and let’s say that we had an error selecting our Pokemon, this is how it is going to display to the user: The error message is specifically tied to the component where the issue occurred, ensuring that it's the only part of your application that shows an error notification. Meanwhile, the rest of your components will continue to operate and display as intended, maintaining the overall user experience without widespread disruption. This targeted error handling keeps the application's functionality intact while indicating where the problem lies. Conclusion stands out as a formidable feature in Vue.js, transforming the management of asynchronous operations into a more streamlined and user-centric process. It not only elevates the user experience by ensuring smoother interactions during data loading phases but also enhances code maintainability and application performance. I hope you found this blog post enlightening and that it adds value to your Vue.js projects. As always, happy coding and continue to explore the vast possibilities Vue.js offers to make your applications more efficient and engaging!...

Rendering Modes in Nuxt 3 cover image

Rendering Modes in Nuxt 3

Nuxt is an open source framework to make your Vue.js applications. Currently, Nuxt 3 has a release candidate and the stable version would be out soon. In this series, I would be taking us through Nuxt 3 concepts and APIs. In this first part, we would focus on rendering modes in Nuxt 3. Setup To quickly install Nuxt 3, lets use the nuxt 3 documentation. Rendering modes Nuxt 3 offers a couple of different rendering modes: - Universal Rendering - Client-side rendering - Hybrid rendering Each of this rendering modes have their use cases and benefits. In this article, we will take a look at their pros, and how to implement them in Nuxt 3. Universal Rendering Universal rendering allows for code to be pre-rendered at build time or rendered on the server before it is served to the client on request. This results in very fast pages, because rendering on the server is faster and the user gets content on page load compared to Client-Side only rendering. Nuxt 3 also improves this greatly by allowing code to be rendered not just on node-js servers, but also on CDN edge workers. This increases the site speed, and also helps reduces cost. In Universal rendering, we can use pre-rendering. This means we can have some pages, or all pages, pre-rendered on build time, and other pages rendered at request time. This is a great choice for static pages like website landing pages, blogs and some dynamic pages that don’t change often and have a finite number of pages. Universal mode with server side rendering is best for sites with highly dynamic content that changes frequently, like ecommerce sites. Pros Search Engine Optimization: Universal rendered apps serve the page with the generated html content to the browser. This makes it easy for web crawlers to index such pages. Performance: The performance of Universal rendered apps are fast compared to Client-side rendered apps because the page already contains the HTML code, and this does not rely on the users device to parse and render the HTML. To deploy and use our Nuxt 3 application on a node-server in universal mode: First in nuxt.config.js file ` By default, SSR is set to true if it is not passed in the nuxt.config.js file. Also, preset is set to ‘node-server’ by default, but this can be changed to suit the deployment environment. For example, Vercel preset should be used for Vercel deployment. You can read here for available deployment presets. When we run our build command: ` This will generate the output folder with the app's entry point file. Next, we can run the file with Node. ` or ` To deploy and use our Nuxt 3 application on a static server in universal mode: First, in our nuxt.config.js file, we specify the pages to be generated: ` Now we can run our generate command: ` This will generate the output folder '.output/public' with the pages files and the related JS and CSS files. We can simply place this folder into any static hosting service, and access our application. To generate our routes async, like a blog website, we can fetch our routes and then pass it to the route during build with this hook. ` Client side rendering Client side rendering involves sending every request to a single file, usually ‘index.html’, with empty content and then linking them to the corresponding JS bundles, allowing the browser perform the parsing and rendering of the HTML. Client side rendering is a great choice for heavily interactive websites with animations, like online gaming sites, Saas sites, etc. Pros Offline support: Client-side rendered apps can run offline once the dependent JavaScript files are downloaded if there is no internet. It is easy to make a client side rendered app a Progressive web app. Cheap: Client-side rendered apps are the cheapest in terms of hosting as you do not need to run a server. They are made up of HTML and JS files which can be served from a static server. To config our Nuxt 3 app to be fully client side rendered: First in nuxt.config.js file ` Now we can run our generate command: ` This will generate the output folder '.output/public/index.html' with the apps entry point file and the related js files. We can simply place this folder onto any static hosting service, and access our application. Hybrid rendering Hybrid rendering is an unreleased Nuxt 3 feature that allows developers to configure different rendering modes for different pages. This should be available later this year, and we will explore it in this series once it is. For more official information about the hybrid rendering discussion. In the next article, we will implement a simple webpage using the universal rendering. The webpage will have a blog which will be prerendered, and also a dynamic user page which will be server rendered. --- I hope this article has been helpful to you. If you encounter any issues, you can reach out to me on Twitter or Github....

Making AI Deliver: From Pilots to Measurable Business Impact cover image

Making AI Deliver: From Pilots to Measurable Business Impact

A lot of organizations have experimented with AI, but far fewer are seeing real business results. At the Leadership Exchange, this panel focused on what it actually takes to move beyond experimentation and turn AI into measurable ROI. Over the past few years, many organizations have experimented with AI, but the challenge today is translating experimentation into measurable business value. Moderated by Tracy Lee, CEO at This Dot Labs, panelists featured Dorren Schmitt, Vice President IT Strategy & Innovation at Allen Media Group, Greg Geodakyan, CTO at Client Command, and Elliott Fouts, CAIO & CTO at This Dot Labs. Panelists discussed how companies are moving from early AI experiments to initiatives that deliver real results. They began by examining how experimentation has evolved over the past year. While many organizations did not fully utilize AI experimentation budgets in 2025, 2026 is showing a shift toward more intentional investment. Structured budgets and clearly defined frameworks are enabling companies to explore AI strategically and identify initiatives with high potential impact. The conversation then turned to alignment and ROI. Panelists highlighted the importance of connecting AI projects to corporate strategy and leadership priorities. Ensuring that AI initiatives translate into operational efficiency, productivity gains, and measurable business impact is essential. Companies that successfully align AI efforts with organizational goals are better equipped to demonstrate tangible outcomes from their investments. Moving from pilots and proofs of concept to production was another major focus. Governance, prioritization, and workflow integration were cited as essential for scaling AI initiatives. One panelist shared that out of nine proofs of concept, eight successfully launched, resulting in improvements in quality and operational efficiency. Panelists also explored the future of AI within organizations, including the potential for agentic workflows and reduced human-in-the-loop processes. New capabilities are emerging that extend beyond coding tasks, reshaping how teams collaborate and how work is structured across departments. Key Takeaways - Structured experimentation and defined budgets allow organizations to explore AI strategically and safely. - Alignment with business priorities is essential for translating AI capabilities into measurable outcomes. - Governance and workflow integration are critical to moving AI initiatives from pilot stages to production deployment. Successfully leveraging AI requires a balance between experimentation, strategic alignment, and operational discipline. Organizations that approach AI as a structured, measurable initiative can capture meaningful results and unlock new opportunities for innovation. Curious how your organization can move from AI experimentation to real impact? Let’s talk. Reach out to continue the conversation or join us at an upcoming Leadership Exchange. Tracy can be reached at tlee@thisdot.co....

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co