Introduction
Converting a Vue.js app into a Progressive Web App (PWA) is a powerful way to enhance its functionality, offering offline access and improved performance. With tools like Vite, Vue, and service workers, you can seamlessly integrate PWA capabilities. In this tutorial, we’ll guide you through the process of transforming your single-page application into a PWA, including setting up a service worker and utilizing IndexedDB for efficient offline data storage. By following these steps, you’ll provide users with a smooth and reliable experience, even without an internet connection.
What is Progressive Web App (PWA)?
A Progressive Web App (PWA) is a type of web application that behaves like a mobile app but can be accessed through a web browser. It offers features like offline access and the ability to be installed on devices, making it more user-friendly and reliable. PWAs use service workers for caching and a web app manifest for installation, allowing users to interact with the app even without an internet connection.
Step 1 – Setting up the project
Alright, let’s get started! The first thing we’re going to do is clone and set up the sample project on your local development environment. Now, this isn’t just any ordinary app. The project uses Vite and Vue.js, which makes for a much faster and smoother development experience. If you’ve used Tailwind CSS before, you’ll love how it’s integrated here to make the interface responsive and sleek, giving the app a polished, modern feel. These technologies work together like a dream team—ensuring that your workflow is efficient and productive.
To start, you’ll need to clone an existing Vue app that was created just for this tutorial. It’s called the What’s New app. Sounds like something you’d read in a magazine, right? It’s a news aggregator that pulls data from the News API, neatly sorting everything into categories like headlines, general news, and even a personalized feed tailored just for you. The goal is to build on this app by adding exciting new features, like turning it into a Progressive Web App (PWA) and giving it offline capabilities. Cool, right?
Let’s go step by step. First, head over to GitHub and find the What’s New app. Once you’re there, click on the Fork button in the top-right corner. This is an important step—it’ll create your own copy of the repository, so you can make changes and track them without affecting the original project.
Once you’ve forked the repo, open up your command line (CLI), and run the following command to clone the repository to your machine:
$ git clone https://github.com/{your-github-username}/whats-new.git
Just make sure to replace {your-github-username} with your actual GitHub username. That will download the project to your local machine so you can start working on it.
Next, go into the project directory by running:
$ cd whats-new
Now, let’s check out the starter code branch to make sure you’re working with the initial setup. You can do this by running:
$ git checkout do/starter-code
If you haven’t registered for the News API and grabbed your API key yet, this is your reminder to do so! Go ahead and sign up, get your API key, and we’ll be ready to start pulling in real news data for the app.
Once you have your API key, open your favorite code editor (I’m a fan of VS Code, but use whatever works for you!), and locate the .env.example file in the project folder. Copy that file and rename it to .env . Inside the .env file, you’ll find a placeholder for VITE_NEWS_API_KEY . Replace that with your actual News API key, and make sure to save your changes!
Now, let’s install the necessary project dependencies. Open your terminal and run:
$ npm install
This will install everything the app needs to run smoothly. Once that’s done, you can fire up the app by running:
$ npm run dev
If all goes well, you should see something like this in the terminal:
[email protected] dev > vite VITE v5.0.10 ready in 3639 ms
➜ Local: http://localhost:5173/
➜ Network: use –host to expose
➜ press h + enter to show help
Now open your browser and navigate to http://localhost:5173/, and you’ll be welcomed with the What’s New homepage, showing the news items fetched from the News API. Congratulations! You’ve just set up your project, and now you’re ready to start adding features and making the app even better!
Progressive Web Apps (PWA) Overview
Step 2 – Creating a web app manifest configuration
So, here’s the deal—when you’re building a Progressive Web App (PWA), there’s one key thing you absolutely need to set up: the web application manifest. Think of it as your app’s identity card on the web. It’s a simple JSON file that holds all the essential details about your app, like its name, icons, and display settings. This is what the browser uses to know how to handle your app when it’s installed on a user’s device. Without it, your PWA will have no idea what to do once it’s installed.
Now, here’s the catch: for the manifest to be legit and actually work, it needs to include four key pieces of information—these are the four “keys” that tell the browser exactly how your app should behave. So, what are these keys? Glad you asked!
- name – This is the full name of your app, usually what shows up when someone installs it on their device. It’s like the title of your app!
- icons – A set of images or icons that represent your app across different devices and screen sizes. You can think of these as the digital version of your app’s face.
- start_url – This is the URL that opens when someone launches the app. Essentially, it’s the app’s starting point.
- display – This defines how the app should be shown on the device, whether in fullscreen mode, standalone, or with a minimal UI. It’s all about how your app looks when it’s opened.
Now, sure, you could create this manifest file manually. But I bet you’d rather keep things simple, right? That’s why we’re going to use a PWA Manifest Generator tool to make everything a lot easier. It’ll generate the manifest and icons for you automatically. No headaches, just smooth sailing.
Here’s how to do it:
- Launch the PWA Manifest Generator Tool: Head over to the PWA Manifest Generator tool. This is where all the magic happens. The tool will take care of creating the manifest and generating all the icons your app will need.
- Configure the Manifest: Once you’ve opened the tool, fill in a few basic details like the name of your app, theme color, background color, and any other info you want to include. You can also tweak how you want the app to appear when installed, like adjusting the display settings or orientation to match your vision.
- Upload the Icon: Next, you’ll be prompted to upload your app’s icon. You can find it in the What’s New project folder as a file called app-icon-image.png . Just upload that file, and the tool will automatically create all the different icon sizes for you—super easy!
- Generate the Manifest: Once you’ve filled out all the fields and uploaded the icon, click the Generate Manifest button. The tool will process everything you’ve entered and give you a zipped folder. This folder will contain the manifest file and all the icons you need.
- Download and Extract the Files: Download the zipped folder, and once it’s on your computer, unzip it. Inside, you’ll find the manifest.webmanifest file and several icon files, each labeled with its size (like icon-192x192.png , icon-512x512.png , etc.). You’ll want to copy these icon files into your app’s /public directory so they’re ready to go when the app is installed.
- Edit the Manifest File: Open the manifest.webmanifest file in your favorite text editor (I like VS Code, but use whatever works best for you). You’ll see a JSON object with all the configuration settings for your app, something like this:
{
“name”: “What’s New”,
“short_name”: “What’s New”,
“description”: “A news aggregator app fetching data from the News API.”,
“icons”: [
{
“src”: “/icon-192×192.png”,
“sizes”: “192×192”,
“type”: “image/png”
},
{
“src”: “/icon-512×512.png”,
“sizes”: “512×512”,
“type”: “image/png”
}
],
“start_url”: “/”,
“display”: “standalone”,
“background_color”: “#ffffff”,
“theme_color”: “#0F172A”
}
This file contains all the vital information you need—like the app name, icons, start URL, and how the app should display when installed. It’s like your app’s digital resume!
Save and Use the Manifest: Once you’ve reviewed the manifest and made any adjustments you need, go ahead and save the file. This JSON object will be your reference in the next steps when you integrate it into the app’s functionality.
And just like that, you’ve successfully created and set up your web app manifest! This is a huge milestone in getting your PWA up and running. Now, when users install your app, they’ll get a smooth, professional experience, and that’s exactly the kind of vibe you want to create!
For more details, refer to the Web App Manifest Specification.
Step 3 – Generating the web app manifest and service worker
Alright, here we go! You’ve made some solid progress on your app, and now it’s time to get it closer to being a fully functional Progressive Web App (PWA). So let’s roll up our sleeves and dive into creating the web application manifest and setting up the service worker.
You’re going to use a Vite plugin called vite-plugin-pwa to make it happen. Think of it like your magic tool that helps you easily add PWA features to your Vite-based app. Before you get started, though, you’ll need to install the plugin first. Don’t worry, it’s simple, and I’ll guide you through it.
Installing the Plugin:
Open your terminal and run this command:
$ npm install -D vite-plugin-pwa
Once it’s installed, the next thing you’ll need to do is configure the plugin in your project. You’ll have to make a quick edit to your vite.config.js file. Don’t worry, it’s a simple tweak.
Here’s what you need to do:
import { defineConfig } from ‘vite’;
import vue from ‘@vitejs/plugin-vue’;
import { VitePWA } from ‘vite-plugin-pwa’;
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
VitePWA({})
],
});
This part of the code sets up vite-plugin-pwa in your project, and that’s what allows it to start working. But hold up—there’s more to do! You still need to tell the plugin what your PWA should look like. This is where you configure the manifest—the file that gives your app its name, icons, and how it should behave when launched.
Updating the vite.config.js for the Manifest:
Now, we need to add a little extra to the config file. Specifically, you’ll add a manifest key inside the VitePWA() function. This is where you’ll link it to the manifest JSON object you created earlier.
Here’s the updated vite.config.js file:
export default defineConfig({
plugins: [
vue(),
VitePWA({
manifest: {
“theme_color”: “#0F172A”,
“background_color”: “#f5f8fa”,
“display”: “standalone”,
“scope”: “/”,
“start_url”: “/”,
“name”: “What’s New – Vue News Aggregator Site”,
“short_name”: “What’s New”,
“description”: “A news aggregator pulling news items from News API.”,
“icons”: [
{ “src”: “/icon-192×192.png”, “sizes”: “192×192”, “type”: “image/png” },
{ “src”: “/icon-256×256.png”, “sizes”: “256×256”, “type”: “image/png” },
{ “src”: “/icon-384×384.png”, “sizes”: “384×384”, “type”: “image/png” },
{ “src”: “/icon-512×512.png”, “sizes”: “512×512”, “type”: “image/png” }
]
}
})
],
});
By adding this, you’ve told the plugin exactly what to do with your PWA manifest. It’s got everything: the app’s name, description, icons, and how it should appear when it’s launched (like in standalone mode). Now your app will know exactly how to show up when users install it.
What Happens Next:
Once you save those changes, here’s what happens every time you build your app: the plugin will automatically generate the web app manifest for the browser and place it in your app’s entry point (/). On top of that, it will also create a service worker. This is the behind-the-scenes helper that manages caching and offline behavior for your app.
Now, how do you check if everything is working? Let’s do a quick inspection of your browser to make sure everything is set up properly.
Verifying Your PWA:
To make sure your app has turned into a PWA, you need to check a few things in your browser’s developer tools. Here’s how:
Opening Developer Tools:
If you’re using Chrome, open your developer tools by pressing:
- CTRL + SHIFT + I on Windows
- OPTION + CMD + I on Mac
Checking the Manifest:
In the Application tab, look for the Manifest section in the sidebar. Click on it, and voilà! You should see the details you added in the manifest—like the app’s name, icon, and start URL.
But here’s the thing: If you’re still in development mode, this might show up empty. Don’t stress, though—that’s normal. The plugin only generates the manifest when the app is built in production mode.
Verifying the Service Worker:
Next, let’s check the Service Workers section. If you’re still in development mode, you won’t see anything here. But once the app is in production, this section will show the service worker the plugin created.
Checking Cache Storage:
Now, head over to the Storage section and look under Cache Storage. In development mode, this will be empty. But once your app is in production, you should see some cached files here, ready to help your app work offline.
Look for the Install Button:
You should also spot the install button in the browser’s toolbar. This little icon allows users to install your app. But here’s the thing: It won’t show up in development mode. You’ll need to build your app in production mode for this button to appear.
Building and Previewing the App:
Let’s take it to the next level. To fully test your PWA, you need to build the app and preview it in production mode. Here’s how:
$ npm run build
$ npm run preview
What happens here? npm run build compiles your project, and npm run preview serves the built application. Once you preview the app, you should see:
- The manifest configuration
- Cached files in Cache Storage
- The install button in the toolbar!
Using devOptions for Dev Mode:
But hey, I get it—rebuilding the app every time you make a change can be a bit of a hassle. So here’s a cool trick: The vite-plugin-pwa package has a devOptions feature that lets you preview your app as a PWA while you’re still in development mode. How awesome is that?
To enable this, all you need to do is add this to your vite.config.js :
export default defineConfig({
plugins: [
vue(),
VitePWA({
devOptions: { enabled: true },
manifest: { … }
})
],
});
Once you save these changes and restart your dev server, you’ll see some extra logs in the console, showing that the service worker is being generated and cached. The sw.js file and related files are now created, and registerSW.js will make sure the service worker gets registered as soon as the app loads in the browser.
After the app loads, you can check the Manifest, Cache Storage, and Service Workers sections to confirm that everything is set up right. The active service worker will now show up in the Service Workers section of your developer tools.
And just like that, your app is one step closer to being a fully functional PWA. From the manifest to the service worker, you’ve set everything up. Now your app is ready to be installed and used offline—just like a native mobile app!
Step 4 – Handling the caching of application files and assets
You’ve made some awesome progress, and now your app is officially a Progressive Web App (PWA). But here’s the thing—it’s still not quite ready to work offline. If you open up the Network tab in your browser’s developer tools, switch it to offline mode, and refresh the page, you’re probably going to see a blank screen. That’s because the app still depends on an internet connection. But no worries, we’re about to fix that and get it working offline.
The Mission: Cache Your App’s Files and Assets
The problem is simple: your app hasn’t been set up to cache its files and assets yet. Without that, there’s no way for your app to work offline. So, we’re going to make a small but essential change in the Vite PWA plugin configuration to enable caching.
Let’s get into the code!
Modifying the Vite Configuration:
You’ll need to update your vite.config.js file by adding some key info to ensure your app caches the right assets. Here’s the code you’ll be working with:
export default defineConfig({
plugins: [
vue(),
VitePWA({
devOptions: {…},
includeAssets: [“**/*”],
manifest: {…},
workbox: {
globPatterns: [“**/*.{js,css,html,png}”]
}
])
});
Explanation of the Code:
Let me break this down for you:
- includeAssets: This tells the plugin to include all the static files from the public folder in the service worker’s precache. These could be files like your app’s favicon, SVG images, and font files. These are important because your app needs them cached in order to work offline.
- globPatterns (under workbox): This is where you specify which file types should be cached by the service worker. In this case, we’re saying: “Hey, cache all the .js, .css, .html, and .png files because these are the main files your app needs to run smoothly.”
Once you add these changes, your app will have all the necessary files and assets it needs to function offline. The service worker will do the job of caching everything for you.
Testing the Offline Functionality:
Okay, you’ve made the changes—now we need to see them in action, right? Let’s rebuild the app and test it in production mode.
If you’re still running the app in development mode, you’ll need to build it first. Run this command to compile your app:
$ npm run build
Once the build is complete, you can preview the production version by running:
$ npm run preview
This will create a production build of your app, allowing you to test the offline functionality more accurately.
Why Does This Process Differ in Development Mode?
Here’s the thing: the development build works a little differently from the production build. In development mode, the app runs in memory and doesn’t save files to disk. So, there’s no output in a dist folder like you get in production. This means the service worker doesn’t have anything to cache in development mode. It can only cache basic files like index.html and registerSW.js.
So, if you try running your app offline in development mode, you’re probably going to see a blank screen since none of the app’s resources are cached.
Verifying Caching in Production Mode:
Here’s where it gets fun. After you run the production build, you can confirm that caching is working by checking the sw.js file in both the dev-dist and dist directories.
To test it:
- Run the production build and open your browser.
- Open Developer Tools and go to the Network tab.
- Set the network to offline mode.
- Refresh the page.
If everything is working, the app should load without issues, even without an internet connection!
Checking the Cached Files:
To check if the service worker is caching everything it should, go to the Application tab in the developer tools. Click on Cache Storage, and you should see an item labeled workbox-precache-*. This will show all the files the service worker has cached. These files should match the contents of your app’s dist folder.
Comparing Caching in Development and Production:
Now, let’s compare how caching behaves in development mode versus production mode. Here are some key differences:
- In production mode: You’ll see the full list of cached files, including your JavaScript, CSS, HTML, and image files. These are the files your app needs to work offline.
- In development mode: The cache is either empty or only contains basic files like index.html and registerSW.js. This happens because development builds don’t save files to disk like production builds do.
Key differences to note:
- Number of entries: In production mode, you’ll see all the files your app needs. In development mode, not so much.
- Port numbers: These will differ depending on whether you’re running in development or production mode.
- File names: In production mode, the file names in the cache should match exactly what’s in your dist folder. In development mode, they might not align because the files aren’t being cached.
Wrapping It Up:
So here’s where we are: you’ve successfully configured your app to cache the essential files and assets, so it can work offline. With the service worker in place and the right files cached, users can now use your app even without an active internet connection. You’re one step closer to a fully functional PWA!
But wait, there’s more to do. Right now, your app still pulls live data from an API, and that data isn’t cached. In the next step, you’ll learn how to integrate IndexedDB to store and cache that dynamic content as well. Your PWA will be even more powerful than before!
For more information, check out the Service Workers Overview.
Step 5 – Caching application data using IndexedDB
Imagine this: you’ve built an awesome Progressive Web App (PWA) that works perfectly when connected to the internet. But now, it’s time to take things up a notch and make it work offline. Sure, you could stop here, but where’s the fun in that? Let’s make it even better. That’s where IndexedDB comes in.
What is IndexedDB?
IndexedDB is like a super-powered storage vault for your web apps. It’s a browser API that stores structured data and binary files (like images or videos) directly in the browser. Think of it like a local database that lives inside the browser, so you can store and pull data even when there’s no internet connection.
In simple terms, IndexedDB lets you keep your app running offline by storing the data it needs. It organizes data in a key-value pair format (kind of like JavaScript objects), which means it’s neat, easy to use, and fast to retrieve.
Step 1: Updating the App to Retrieve Data from an API
Here’s where we’re at: the app currently pulls data from local variables. But to make it offline-ready, we need to fetch real data from an API and store it in IndexedDB for future use.
Here’s what you need to do:
- Open the NewsItems.vue file and follow these steps:
Remove the Hardcoded Data:
Get rid of that old test data variable ( testNewsItemsData ). It’s not needed anymore because we’re about to fetch real data from the API.
Modify the getCustomizedTabNewsItems Function:
Find the function getCustomizedTabNewsItems inside the <script setup> tag. You’ll see a block of code like this:
const getCustomizedTabNewsItems = () => {
//…
if (definedCustomizations) {
//…
/** TODO: Remove the line below after setting up your API KEY and delete this comment */
newsItems.value = [
{
source: { id: ‘buzzfeed’, name: ‘Buzzfeed’ },
//…
},
];
/** TODO: Uncomment after setting up your API KEY */
// const { fetchedNewsItems, getNewsItems } = useNewsItems(requestUrl);
// nextTick(async () => {
// await getNewsItems();
// newsItems.value = fetchedNewsItems.value;
// });
}
};
Uncomment the Code to Fetch API Data:
After setting up your API key, uncomment the code that fetches the real-time data. This will allow your app to get news from the News API instead of using static placeholders.
Update the Logic for Custom News Categories:
Replace the hardcoded test data with API data:
if (props.tab.id === APPLICATION_TABS[2].id && props.retrieveCustomCuratedContent) {
// …
} else if (props.tab.id === APPLICATION_TABS[2].id && !props.retrieveCustomCuratedContent) {
// …
} else {
// Get news items for other tabs…
/** TODO: Remove the line below after setting up your API KEY and delete this comment */
newsItems.value = testNewsItemsData;
/** TODO: Uncomment after setting up your API KEY */
// const { fetchedNewsItems, getNewsItems } = useNewsItems(requestUrl);
// nextTick(async () => {
// await getNewsItems();
// newsItems.value = fetchedNewsItems.value;
// });
}
Open the SourceToggleTokens.vue file and uncomment the necessary code that allows the app to fetch news from different sources.
Step 2: Installing and Setting Up IndexedDB with idb
Now for the fun part: IndexedDB. But before we jump in, we need some help. That’s where idb comes in. It’s a lightweight wrapper around IndexedDB that makes it way easier to work with.
Run the following command to install idb using npm:
$ npm install idb
This package helps you interact with IndexedDB without all the complicated stuff.
Step 3: Creating the useIDB Composable
Next, let’s create a new composable file called useIDB.js . This file will handle all the magic of interacting with IndexedDB.
Here’s the basic setup:
import { openDB } from ‘idb’;
import { ref } from ‘vue’;const versionNumber = ref(1);const useIDB = () => {
const db = ref(null); const getDB = async (version, objectStoreName, keyPath) => {
versionNumber.value += 1;
db.value = await openDB(‘whats-new’, version, {
upgrade(db, oldVersion) {
if (version === 1 && oldVersion === 0) {
db.createObjectStore(objectStoreName, { keyPath });
}
if (version > 1) {
if (!db.objectStoreNames.contains(objectStoreName)) {
db.createObjectStore(objectStoreName, { keyPath });
}
}
},
});
}; return { db, versionNumber, getDB };
};export default useIDB;
What’s Going on Here?
- db: This is a reference to your IndexedDB database.
- getDB: This function opens or creates an IndexedDB database. It requires a version, object store name, and a key path (a unique identifier for each record).
- versionNumber: This keeps track of the database version. It ensures that the database is updated correctly when changes are made.
Step 4: Storing and Retrieving Data from IndexedDB
Now, let’s get to the fun part. You can store and retrieve data from IndexedDB.
Here’s how you’ll update the getNewsItems function in useNewsItems.js to store the fetched data in IndexedDB:
async function getNewsItems() {
const { db, getDB, versionNumber, getDataFromObjectStore } = useIDB(); try {
const apiResponse = await fetch(url.value, {
headers: {
‘X-Api-Key’: import.meta.env.VITE_NEWS_API_KEY,
},
}); const data = await apiResponse.json();
// Store fetched items in IndexedDB
await getDB(versionNumber.value, url.value, ‘url’);
data.articles.forEach(async (article) => {
await db.value.put(url.value, article);
});
} catch (error) {
if (error instanceof TypeError && error.message.includes(‘Failed to fetch’)) {
const cachedItems = await getDataFromObjectStore(url.value);
fetchedNewsItems.value = cachedItems;
}
}
}
How It Works:
- If the API fetch is successful, the articles are stored in IndexedDB.
- If the fetch fails (e.g., no internet), the app pulls the data from IndexedDB, so it keeps working offline.
Step 5: Testing Your Changes
Let’s make sure everything is working smoothly. Rebuild the app:
$ npm run build
$ npm run preview
Once the build is ready, open the app and go to the IndexedDB section in your browser’s developer tools. You should see your data stored there!
To test offline functionality, set the network throttling to offline in the Network tab and refresh the page. If everything’s set up right, you should see the news items loading from IndexedDB, even without an internet connection.
Congrats! You’ve just added offline functionality to your PWA, making it able to cache dynamic data from the News API using IndexedDB. Now, whether users are online or offline, they’ll still be able to enjoy your app. Nice work!
Conclusion
In conclusion, transforming a Vue.js app into a fully functional Progressive Web App (PWA) with Vite, a service worker, and IndexedDB significantly enhances the user experience by enabling offline access and improved performance. By following the steps outlined in this tutorial, you’ve learned how to configure your app’s manifest, set up a service worker, and utilize IndexedDB for caching both static and dynamic data. This setup ensures that users can seamlessly interact with your app, even without an internet connection.Looking ahead, as web technologies continue to evolve, the integration of PWAs will play an even more critical role in creating fast, reliable, and engaging web experiences. Stay up-to-date with emerging trends in PWA development to keep your app at the forefront of web innovation.
Docker system prune: how to clean up unused resources (2025)