Add PWA Icons to Your Project
Where the icons, manifest and tags from the generator go in plain HTML, React with Vite, Next.js and Angular, and how to check that they work.
What is in the ZIP
The same files work in every framework. Only two things change from one project to the next: the folder that is served at the root of your site, and where the head tags are written.
| File | Where it goes |
|---|---|
| icons/ | The folder served at the site root, so the icons load at /icons/icon-192x192.png. |
| splash/ | The same folder, if you generated iOS splash screens. |
| manifest.json | The same folder, so it loads at /manifest.json. |
| favicon.ico | The same folder, so it loads at /favicon.ico. In Next.js it goes in app/ instead. |
| meta-tags.html | Not deployed. Copy its tags into the head of your page. |
| preview.html and README.md | Not deployed. Open preview.html to see the icons in use before you copy them. |
No files yet? The PWA icon generator makes all of them from one image, with your app name and colors already filled in.
Plain HTML site
Copy icons/, splash/, manifest.json and favicon.ico into the folder that holds your index.html, which is the root of your site. Then paste the contents of meta-tags.html into the head of every page, not only the home page, because the browser looks for the manifest link on the page it is on.
Folder layout
my-site/
├── index.html
├── favicon.ico
├── manifest.json
├── icons/
│ ├── icon-192x192.png
│ └── ...
└── splash/
index.html head
<link rel="manifest" href="/manifest.json">
<link rel="icon" href="/favicon.ico" sizes="48x48">
<link rel="icon" href="/icons/icon-192x192.png" type="image/png" sizes="192x192">
<link rel="apple-touch-icon" href="/icons/icon-180x180.png">
<meta name="theme-color" content="#3b82f6">
React, Vue or Svelte with Vite
Vite serves the public/ folder at the site root and copies it to the build unchanged. Copy icons/, splash/, manifest.json and favicon.ico into public/, and paste the tags from meta-tags.html into the head of index.html, which sits in the project root, not in public/.
Do not import the icons from your components. Imported files get hashed names in the build, and the manifest would no longer find them. Create React App projects work the same way, with the tags in public/index.html.
If you use vite-plugin-pwa, it writes the manifest and adds its link tag for you. In that case skip manifest.json and the manifest link, still copy the icons into public/, and move the icons array from the generated manifest.json into the plugin options.
Folder layout
my-app/
├── index.html ← paste the tags here
├── public/
│ ├── favicon.ico
│ ├── manifest.json
│ ├── icons/
│ └── splash/
└── src/
vite.config.ts (only with vite-plugin-pwa)
VitePWA({
manifest: {
name: 'My PWA App',
short_name: 'App',
theme_color: '#3b82f6',
background_color: '#ffffff',
display: 'standalone',
icons: [
{ src: '/icons/icon-192x192.png', sizes: '192x192', type: 'image/png', purpose: 'any' },
{ src: '/icons/icon-512x512.png', sizes: '512x512', type: 'image/png', purpose: 'any' },
{ src: '/icons/icon-192x192-maskable.png', sizes: '192x192', type: 'image/png', purpose: 'maskable' },
{ src: '/icons/icon-512x512-maskable.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' },
],
},
})
Next.js
Copy icons/, splash/ and manifest.json into public/. Put favicon.ico in app/, replacing the one a new project starts with, and Next.js adds its tag for you. Do not keep a second copy in public/.
With the App Router, the head is not written by hand. Declare the manifest, icons, Apple settings and theme color in app/layout.tsx with the metadata and viewport exports. For splash screens, add one startupImage entry for each apple-touch-startup-image tag in meta-tags.html: its href becomes url and its media stays as it is.
With the Pages Router, paste the tags into the Head of pages/_document.tsx instead, closing each one with /> as JSX requires.
app/layout.tsx (App Router)
import type { Metadata, Viewport } from 'next';
export const metadata: Metadata = {
title: 'My PWA App',
manifest: '/manifest.json',
icons: {
icon: [
{ url: '/icons/icon-192x192.png', sizes: '192x192', type: 'image/png' },
{ url: '/icons/icon-512x512.png', sizes: '512x512', type: 'image/png' },
],
apple: '/icons/icon-180x180.png',
},
appleWebApp: {
capable: true,
title: 'My PWA App',
statusBarStyle: 'default',
startupImage: [
{
url: '/splash/splash-1290x2796.png',
media: '(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)',
},
// ...one entry per splash screen in meta-tags.html
],
},
};
export const viewport: Viewport = {
themeColor: '#3b82f6',
};
pages/_document.tsx (Pages Router)
import { Html, Head, Main, NextScript } from 'next/document';
export default function Document() {
return (
<Html lang="en">
<Head>
<link rel="manifest" href="/manifest.json" />
<link rel="icon" href="/favicon.ico" sizes="48x48" />
<link rel="apple-touch-icon" href="/icons/icon-180x180.png" />
<meta name="theme-color" content="#3b82f6" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
Angular
Projects created with Angular 17 or later serve the public/ folder at the site root. Copy icons/, splash/, manifest.json and favicon.ico into public/, replacing the favicon that is already there, and paste the tags from meta-tags.html into the head of src/index.html. Older projects use src/assets/ instead, and every file must be listed under assets in angular.json.
If you ran ng add @angular/pwa, the project already has public/manifest.webmanifest, a link to it in src/index.html and placeholder icons. Copy the generated icons/ folder over the placeholder one, then replace the contents of manifest.webmanifest with the contents of manifest.json. Keep the .webmanifest name, because index.html and the service worker already refer to it, and do not paste a second manifest link.
If the app is deployed under a sub-path with --base-href, remove the leading slash from every path in the tags and the manifest, so that icons/icon-192x192.png resolves against the base.
src/index.html head
<base href="/">
<link rel="manifest" href="manifest.webmanifest">
<link rel="icon" href="/favicon.ico" sizes="48x48">
<link rel="apple-touch-icon" href="/icons/icon-180x180.png">
<meta name="theme-color" content="#3b82f6">
Check that it works
Deploy the site and open /manifest.json and /icons/icon-512x512.png directly in the browser. You should see JSON and an image. If you see your app instead, the files are not in the folder that is served at the root, and a single-page app fallback is answering for them.
Then open the page in Chrome, choose Application in DevTools and look at the Manifest panel. It shows every icon it loaded and any installability warning.
For a full check of a live site, enter its address in the PWA validator, which loads the manifest and every icon and says which install requirement fails, if any.
Frequently Asked Questions
Where do I put manifest.json?
In the folder your framework serves at the site root: public/ in Vite, Create React App, Next.js and Angular 17 or later, or next to index.html on a plain site. Open /manifest.json on the live site to confirm it loads as JSON.
Do I need to import the icons in my code?
No. Files in public/ are served by their path. Importing them gives them hashed file names in the build, and the paths in the manifest no longer match.
Should the file be called manifest.json or manifest.webmanifest?
Either works. Browsers read the file the manifest link points to, whatever its name. Angular uses manifest.webmanifest, and if your server does not recognise that extension, set its content type to application/manifest+json.
Why do my icons work locally but not after deploying?
Usually because the files were not copied to the build, or the site is served from a sub-path and the paths start with a slash. Open an icon address on the live site: if it shows your app instead of the image, the file is missing from the deployed folder.