Install
Nothing to install for a one off run: npx downloads the CLI. For CI and repeat builds add it as a dev dependency. Node 18 or newer.
npx installkite init
# or keep it in the project for CI builds
npm install --save-dev installkiteThe runtime script also ships on the CDN, pinned to a version:
<script src="https://cdn.jsdelivr.net/npm/installkite@0.1.0/dist/installkite.min.js" data-sw="/sw.js" data-install="auto" defer></script>Quick start
- Run init in the site folder. It finds your pages, your logo and your colors and asks a few questions. Add
--yesto accept the detected values. - Check it.
npx installkite doctorlists every installability and offline rule with a fix for each failure. - Deploy over HTTPS. GitHub Pages, Netlify, Vercel and Cloudflare Pages all qualify. Open the site and the browser offers to install it.
- After each site build run
npx installkite build(frameworks get it added to the build script) so the service worker precaches the new files.
npx installkite init --dry-run # preview every change as a diff
npx installkite init --yes # write it
npx installkite doctor # auditinstallkite init
Scans the project, then writes the manifest, icons, offline page, runtime, service worker and config, and injects the head tags into your HTML. It is idempotent: the tags live between <!-- installkite:start --> and <!-- installkite:end -->, a second run updates that block in place, and tags your page already has (a favicon, a manifest link, a theme color) are kept and not duplicated.
| Flag | What it does |
|---|---|
--root <dir> | The deployed folder that gets precached. Detected: the folder with index.html, dist for Vite and Astro, out for Next.js, public for Laravel. |
--public <dir> | Where generated files are written, so a framework copies them into every build. public for Vite, Next.js and Astro. |
--base <path> | The URL path the site is served under, like /my-repo/ on GitHub Pages. Detected from the git remote and framework config. |
--relative | Write relative URLs in head tags (./, ../) so the site works from any folder. |
--icon <file> | Source image, PNG or SVG, ideally 512 pixels or larger. Without one, a monogram of the initials is drawn. |
--name, --short-name, --description | Manifest text. The short name is the home screen label, 12 characters or fewer. |
--theme-color, --theme-color-dark, --background-color | Any CSS color. The theme color defaults to the page meta tag, then to the dominant color of the icon. |
--display <mode> | standalone (default), fullscreen, minimal-ui or browser. |
--start-url, --scope, --lang, --orientation | Manifest fields. start_url and scope default to ./. |
--cdn | Link installkite.js from jsDelivr instead of copying it into the site. |
--no-runtime | Leave out installkite.js and register the service worker yourself. |
--no-inject | Do not edit HTML files. The tags to paste are printed instead. |
--no-install-button, --no-update-toast, --no-offline-indicator | Turn off parts of the runtime interface. |
--no-offline-page, --no-screenshots, --no-shortcuts | Skip the offline page, the placeholder screenshots or the shortcuts taken from your navigation. |
--no-postbuild | Do not append npx installkite build to the package.json build script. |
-y, --yes | Accept detected values without questions. Also the behavior without a terminal. |
-n, --dry-run | Print a unified diff of every text file that would change. Nothing is written. |
$ npx installkite init --dry-run
create icons/icon-48.png 905 B icon
create icons/icon-72.png 1.4 kB icon
create icons/icon-96.png 1.9 kB icon
create icons/icon-128.png 2.6 kB icon
create icons/icon-144.png 3.0 kB icon
create icons/icon-152.png 3.0 kB icon
...
--- a/about.html
+++ b/about.html
@@ -6,6 +6,17 @@
<title>About | Acme Bakery</title>
<meta name="description" content="Bread, cakes and coffee, baked every morning.">
<link rel="stylesheet" href="style.css">
+ <!-- installkite:start -->
+ <link rel="manifest" href="/manifest.webmanifest">
+ <meta name="theme-color" content="#e8590d">
+ <link rel="icon" href="/favicon.ico" sizes="48x48">
+ <link rel="icon" href="/icons/icon.svg" type="image/svg+xml">
+ <link rel="apple-touch-icon" href="/apple-touch-icon.png">
+ <meta name="mobile-web-app-capable" content="yes">
+ <meta name="apple-mobile-web-app-title" content="Acme Bakery">
+ <meta name="apple-mobile-web-app-status-bar-style" content="default">
+ <script src="/installkite.js" data-sw="/sw.js" data-install="auto" data-offline="badge" defer></script>
+ <!-- installkite:end -->
</head>
<body>
<header>installkite build
Run it after every site build. It hashes the deployed folder again, rewrites sw.js there with the new precache list, and injects the head tags into pages a site generator rebuilt without them. Browsers see a changed worker, download only the files whose hash changed and show the update toast.
# package.json
"scripts": {
"build": "vite build && npx installkite build"
}--check writes nothing and exits with code 1 when sw.js is out of date, which makes a cheap CI guard:
- run: npm run build
- run: npx installkite build --checkinstallkite doctor
Audits the files on disk (served as a static host would, under your base path) or a live site with --url https://example.com/. Exits with code 1 when a rule fails with an error, and with --strict also on warnings. --json prints the report for scripts.
| Rule | Checks | Level |
|---|---|---|
https | The site is HTTPS or localhost (live), or the configured production URL is HTTPS (files). | error |
page, viewport | The start page loads; it has a viewport meta tag. | error, warn |
manifest-link, manifest-fetch, manifest-type | A manifest is linked, loads, is valid JSON and is served as JSON. | error, warn |
manifest-name, short-name | A name exists; the label fits in 12 characters. | error, warn |
display | standalone, fullscreen or minimal-ui (or a display_override). | error |
start-url, scope, start-url-origin, start-url-reachable | start_url is set, same origin, inside scope and loads. | error |
id, description | Stable app id; a description. | info |
theme_color, background_color | Present and valid CSS colors. | warn, error |
icons, icon-file, icon-size | 192 and 512 pixel icons; every file loads; real sizes match the declared ones. | error, warn |
maskable, maskable-opaque, purpose | A maskable icon exists, is full bleed, and is not mixed as "any maskable". | warn |
screenshots | Screenshots exist and are no longer the installkite placeholders. | info, warn |
theme-color-meta, apple-touch-icon, favicon | Head tags: theme color, a 180 pixel opaque apple-touch-icon, a favicon. | info, warn |
sw-registered, sw-fetch, sw-scope, sw-fetch-handler | A worker is registered, loads as JavaScript, controls start_url and handles fetch. | error |
offline-page, start-url-precached | The offline page exists and is precached; the start page opens offline. | warn |
precache-fresh | Files on disk: the precache list matches the current file hashes. | error |
{
"url": "https://site.installkite.local/",
"mode": "local",
"checks": [
{
"id": "https",
"title": "Served over HTTPS",
"status": "info",
"message": "checked when you run doctor --url against the live site",
"fix": "Deploy over HTTPS (localhost also works while developing)."
},
{
"id": "viewport",
"title": "Viewport meta tag",
"status": "pass",
"message": "the page sets a viewport"
},
{
"id": "manifest-link",
"title": "Page links a web app manifest",
"status": "pass",
"message": "links /manifest.webmanifest"
},
"...": "one entry per rule"
],
"errors": 0,
"warnings": 1,
"installable": true,
"offlineReady": true
}icons and remove
installkite icons <image> writes only the icon set, for projects that already have a manifest. --out sets the folder, --background the color behind maskable icons and the apple-touch-icon, --screenshots adds placeholder screenshots.
npx installkite icons brand/logo.svg --out public --background "#0b1426"installkite remove takes the marked block out of every HTML file. Generated files stay where they are.
What gets written
| File | What it is |
|---|---|
manifest.webmanifest | id, name, short_name, description, lang, start_url, scope, display, theme_color, background_color, icons, shortcuts and screenshots. |
icons/icon-48.png to icon-512.png | Ten sizes with purpose any, transparency kept. |
icons/maskable-192.png, maskable-512.png | Full bleed background, the artwork scaled so its farthest visible pixel sits inside the 40% radius safe zone. |
icons/icon.svg | The SVG source, used as a scalable favicon, when you started from SVG. |
apple-touch-icon.png | 180 by 180, opaque, padded for transparent artwork. |
favicon.ico, favicon-16x16.png, favicon-32x32.png | The ICO holds 16, 32 and 48 pixel PNG images. |
icons/screenshot-wide.png, screenshot-narrow.png | Placeholders for the richer install dialog. Replace them with real captures; doctor reminds you. |
offline.html | Self contained offline page in your colors, with the icon inlined and links to saved pages. |
installkite.js | The runtime, about 4.4 kB gzipped. |
sw.js | The service worker: one settings line with the precache list, then the strategies. |
installkite.config.json | What init decided, so build and doctor run without questions. |
Config file
installkite.config.json sits next to your package.json. Edit it and run init --yes again to apply changes; a JSON schema at config.schema.json gives editors completion.
{
"$schema": "https://kwy404.github.io/installkite/config.schema.json",
"root": ".",
"publicDir": ".",
"base": "/",
"icon": "logo.svg",
"iconsDir": "icons",
"html": [
"**/*.html"
],
"app": {
"name": "Acme Bakery",
"display": "standalone",
"themeColor": "#e8590d",
"backgroundColor": "#ffffff",
"startUrl": "./",
"scope": "./",
"screenshots": true,
"description": "Bread, cakes and coffee, baked every morning.",
"lang": "en",
"shortcuts": [
{
"name": "Menu",
"url": "./menu.html"
},
{
"name": "About",
"url": "./about.html"
}
]
},
"runtime": {
"mode": "local",
"installButton": "auto",
"updateToast": true,
"offlineIndicator": true
},
"sw": {},
"placeholders": {
"icons/screenshot-wide.png": "02ac7523e15a",
"icons/screenshot-narrow.png": "7698c19e98aa"
}
}| Key | Meaning |
|---|---|
root | The deployed folder, precached by build. |
publicDir | Where generated files are written. |
base, relative | URL base path, or relative URLs per page. |
url | Your production URL; doctor checks that it is HTTPS. |
icon, iconsDir | Icon source (or "monogram") and the icon folder. |
html | Files or globs that get head tags. Plain sites use "**/*.html", so new pages are covered. |
app | Manifest values: name, shortName, description, lang, display, themeColor, themeColorDark, backgroundColor, startUrl, scope, orientation, categories, shortcuts, screenshots. |
runtime | mode (local, cdn or none), installButton, updateToast, offlineIndicator. |
sw | Service worker options, listed below, plus include, exclude and maxFileSize for the precache. |
Runtime script
One tag, configured with data attributes. It registers the worker once the page has loaded, so it never competes with your own resources.
<script src="/installkite.js" data-sw="/sw.js" data-install="auto" data-offline="badge" defer></script>| Attribute | Meaning |
|---|---|
data-sw | Service worker URL. Default: sw.js next to installkite.js. |
data-scope | Registration scope. Default: the worker folder. |
data-install="auto" | A floating install button when the site can be installed. Dismissing it hides it for 14 days. |
data-update="none" | No "Refresh to update" toast (listen to the update event instead). |
data-offline="badge" | A small badge while the device is offline. |
data-manual | Do not register the worker; call installkite.register() yourself. |
Any element with data-installkite-install becomes an install button: it stays hidden until the site can be installed and opens the prompt, or the iOS sheet, on click.
<button class="btn" data-installkite-install hidden>Install the app</button>The interface follows your theme-color meta tag. Override it with CSS variables:
:root {
--installkite-accent: #2b8ff0; /* buttons */
--installkite-on-accent: #ffffff; /* text on buttons */
--installkite-surface: #ffffff; /* toast, sheet, button bar */
--installkite-text: #14213d;
--installkite-surface-dark: #18202f;
--installkite-text-dark: #eef2f8;
}Runtime API
// installkite.js from the script tag defines window.installkite.
// With a bundler: import installkite from 'installkite/runtime'; installkite.init();
installkite.on('installable', () => {
document.querySelector('#install').hidden = false;
});
document.querySelector('#install').addEventListener('click', async () => {
const outcome = await installkite.install(); // accepted, dismissed, instructions (iOS)
console.log('install:', outcome);
});
installkite.on('installed', () => console.log('Welcome to the home screen'));
installkite.on('update', ({ apply }) => showMyBanner('New version', apply));
installkite.on('offline', () => document.body.classList.add('is-offline'));
installkite.on('online', () => document.body.classList.remove('is-offline'));
// Save pages for offline reading.
await installkite.cache(['/articles/one/', '/articles/two/']);| Member | What it does |
|---|---|
init(options) | Reads options and starts. The script tag build calls it for you with the data attributes. Options: sw, scope, install, update, offline, manual, snoozeDays, text. |
register(url?, scope?) | Registers the service worker, resolves to the registration or null. |
install() | Shows the browser prompt and resolves to accepted or dismissed; on iOS shows the sheet and resolves to instructions; otherwise unavailable. |
update() | Activates a waiting worker and reloads once it takes over. |
cache(urls) | Saves pages into the offline page cache, for "read later" buttons. Resolves to the number saved. |
showIOSInstructions() | Opens the Add to Home Screen sheet. |
on(event, fn), off(event, fn) | Events: ready, installable, installed, update, updated, offline, online, error. on returns an unsubscribe function. Each is also a DOM event named installkite:<event> on window. |
canInstall, isInstalled, isIOS, online, registration, version | Read only state. |
Every interface string can be translated through text:
installkite.init({
text: {
install: 'Instalar app',
iosTitle: 'Adicione a Tela de Inicio',
iosStep1: 'Toque em Compartilhar na barra',
iosStep2: 'Toque em Adicionar a Tela de Inicio',
iosStep3: 'Toque em Adicionar',
update: 'Nova versao disponivel',
refresh: 'Atualizar',
offline: 'Voce esta offline',
online: 'Conectado de novo',
},
});Service worker
The generated sw.js starts with one line of settings, then the strategies. Precached files are stored in installkite-precache-<version>, where the version is a hash of every file hash; runtime caches are installkite-pages, installkite-assets and installkite-images.
| Request | Strategy |
|---|---|
| A precached file (no query string) | Cache first from the precache. Install downloads each file once and copies unchanged ones from the previous version. |
| A page navigation inside the scope | Network first, raced against navigationTimeout. On failure or timeout: the cached page (also tried as /page/, /page/index.html, /page.html), then the offline page. Successful pages refresh the page cache. Navigation preload is used when the browser has it. |
| Scripts, styles, fonts, JSON, manifests (same origin or listed CDNs) | Stale while revalidate, capped at assetMaxEntries. |
| Images | Cache first with imageMaxEntries and imageMaxAge. Opaque cross origin images are not cached. |
| Anything else: POST, range requests, other origins, excluded paths | Not handled; the browser fetches normally. |
| sw option | Default | Meaning |
|---|---|---|
offline | "offline.html" | The fallback page, or null. |
navigationTimeout | 3000 | Milliseconds before a slow page falls back to the cache. |
imageMaxEntries, imageMaxAge | 60, 2592000 | Image cache size and max age in seconds (30 days). |
assetMaxEntries, pageMaxEntries | 120, 50 | Caps for the runtime caches. |
exclude | [] | Path prefixes relative to the scope, or "/regex/" strings, never handled. For example "api/". |
crossOrigin | font hosts and CDNs | Origins whose scripts, styles and fonts are cached. |
skipWaiting | false | Activate new versions at once instead of after "Refresh". |
importScripts | [] | Your own worker code, for push or background sync handlers. |
include, exclude, maxFileSize | HTML, CSS, JS, fonts, key icons; 2 MiB | Which built files are precached. |
/* Service worker generated by installkite 0.1.0: https://kwy404.github.io/installkite/
* Run "npx installkite build" after every site build to refresh the precache list. */
self.__INSTALLKITE__ = {
"version": "8e1ebe2623",
"precache": [
["about.html","ec810b41b7a8"],
["apple-touch-icon.png","821006243b6d"],
["favicon-32x32.png","4e48f2b8460a"],
["favicon.ico","5fc5d0cfdb3f"],
/* 9 more files, each with its content hash */
]
};
/* the bundled strategies follow, about 7 kB */Base paths and scope
The manifest uses relative URLs ("start_url": "./", "scope": "./"), so it works wherever it is served. The head tags need a base: / for a domain root, /repo/ for a GitHub Pages project site. init detects it from package.json homepage, a Vite or Astro base, a Next.js basePath, or a GitHub remote with Pages evidence (a Pages workflow, a docs/ site or the gh-pages package); a CNAME file means a custom domain at /. With --relative every page gets its own prefix such as ../.
Recipes
Plain HTML
Run init in the folder with index.html. Every page gets the tags, including pages you add later when you run build.
cd my-site
npx installkite init --yes
npx installkite doctorVite
Generated files go to public/, the tags into the source index.html, and the build script gets npx installkite build so dist/sw.js precaches the hashed bundles. A base in vite.config is picked up.
npx installkite init --yes
npm run build # vite build && npx installkite buildimport { defineConfig } from 'vite';
export default defineConfig({
base: '/my-repo/', // GitHub Pages project site
});Next.js (static export)
With output: 'export' the site builds to out/. init writes the files to public/, adds npx installkite build to the build script and prints the tags; put them in the root layout.
import type { Metadata, Viewport } from 'next';
import Script from 'next/script';
export const viewport: Viewport = { themeColor: '#2b8ff0' };
export const metadata: Metadata = {
manifest: '/manifest.webmanifest',
icons: { icon: '/favicon.ico', apple: '/apple-touch-icon.png' },
appleWebApp: { title: 'Acme', statusBarStyle: 'default' },
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<Script src="/installkite.js" data-sw="/sw.js" data-install="auto" strategy="afterInteractive" />
</body>
</html>
);
}Astro
Astro builds to dist/ and copies public/. Paste the printed tags into your base layout; base from astro.config.mjs is detected.
---
const { title } = Astro.props;
---
<html lang="en">
<head>
<meta charset="utf-8" />
<title>{title}</title>
<link rel="manifest" href="/manifest.webmanifest" />
<meta name="theme-color" content="#2b8ff0" />
<link rel="icon" href="/favicon.ico" sizes="48x48" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<script is:inline src="/installkite.js" data-sw="/sw.js" data-install="auto" defer></script>
</head>
<body><slot /></body>
</html>WordPress
Run init in your theme folder with --public ., upload the generated files to the site root (the worker must sit at /sw.js to control the whole site) and print the tags from wp_head. Exclude the admin and API from the worker.
// functions.php
add_action('wp_head', function () {
echo '<link rel="manifest" href="/manifest.webmanifest">' . "\n";
echo '<meta name="theme-color" content="#2b8ff0">' . "\n";
echo '<link rel="apple-touch-icon" href="/apple-touch-icon.png">' . "\n";
echo '<script src="/installkite.js" data-sw="/sw.js" data-install="auto" defer></script>' . "\n";
});"sw": { "exclude": ["wp-admin/", "wp-json/", "wp-login.php"] }Laravel
Laravel serves public/, which init detects as both the root and the output folder. Put the tags in your main Blade layout and keep API and auth routes out of the worker.
<head>
<meta charset="utf-8">
<link rel="manifest" href="{{ asset('manifest.webmanifest') }}">
<meta name="theme-color" content="#2b8ff0">
<link rel="apple-touch-icon" href="{{ asset('apple-touch-icon.png') }}">
<script src="{{ asset('installkite.js') }}" data-sw="{{ asset('sw.js') }}" data-install="auto" defer></script>
</head>"sw": { "exclude": ["api/", "login", "logout", "sanctum/"] }GitHub Pages
A project site lives at https://user.github.io/repo/, so every URL needs /repo/. init reads the repository from your git remote and writes the base for you; a CNAME file switches back to /. Turn on "Enforce HTTPS" in the Pages settings.
npx installkite init --yes # base detected from the git remote
npx installkite init --yes --base /repo/ # or set it yourselfNetlify
Run the build step after your site build and serve the manifest with the right type. Netlify already serves HTTPS.
[build]
command = "npm run build && npx installkite build"
publish = "dist"
[[headers]]
for = "/manifest.webmanifest"
[headers.values]
Content-Type = "application/manifest+json"
[[headers]]
for = "/sw.js"
[headers.values]
Cache-Control = "no-cache"Node API
Every piece of the CLI is exported, typed, for build tools and scripts.
import { applyPlan, doctorLocal, generateIcons, loadIconSource, planBuild, planInit } from 'installkite';
import { readFileSync } from 'node:fs';
// The same as npx installkite init --yes, from a script.
const plan = planInit(process.cwd(), { name: 'Acme Bakery', themeColor: '#e8590c', postbuild: false });
for (const file of plan.files) if (file.diff) console.log(file.diff);
applyPlan(process.cwd(), plan.files);
// After a site build:
applyPlan(process.cwd(), planBuild(process.cwd()).files);
// Only the icons, in memory:
const source = loadIconSource(readFileSync('logo.svg'), 'logo.svg');
const { files } = generateIcons(source, { background: '#ffffff' });
const report = await doctorLocal(process.cwd());
if (!report.installable) process.exitCode = 1;| Export | What it does |
|---|---|
planInit(cwd, answers), planBuild(cwd), applyPlan(cwd, files) | The init and build pipelines. Plans are lists of files with their new content and a diff; nothing is written until applyPlan. |
generateIcons(source, options) | The icon set from loadIconSource(bytes). Returns PNG, ICO and SVG files. |
maskableScale(radius), maskablePadding(size, radius), inSafeZone(x, y, size) | The safe zone math. |
generateManifest(options) | A manifest object with fields in a readable order. |
injectHead(html, options), removeHead(html) | Idempotent head tag injection. |
buildPrecache(root, options), generateServiceWorker(entries, options) | The hashed precache list and sw.js. |
createRouter(config, env) | The service worker strategies against injected caches and fetch, for your own worker or tests. |
doctorLocal(cwd), doctorRemote(url), runDoctor(context) | The audit, on files or a live URL. |
rasterizeSvg(svg, options), decodePng, encodePng, resize, encodeIco | The image toolkit, pure JavaScript. |
Limits
- SVG sources are drawn by a built in rasterizer that covers what logos use: paths, shapes, gradients, strokes and dashes, transforms, opacity, clip paths,
use, simple<style>rules and embedded PNG images.<text>, masks, filters and patterns are skipped with a warning: convert text to outlines before exporting. - Raster sources must be PNG for the CLI. The online generator also reads JPEG and WebP through the browser.
- iOS has no install prompt, no theme color for the status bar in standalone mode beyond the status bar style, and clears storage of sites unused for weeks; installed apps keep theirs.
- Firefox on desktop does not install sites. The service worker and offline page still work there.
- Opaque responses (cross origin without CORS) are never cached, to avoid filling storage quotas.