create-web-manifest
One source mark in, a shippable install identity out: every icon size, a complete
manifest.webmanifest, and a <head> block — then an audit that proves it.
Run
Determine this SKILL.md's own directory as {baseDir}. Dry-run first, always:
node "{baseDir}/create-web-manifest.mjs" <project-dir> --dry-run
It prints the framework it found, the mark it will use, the colours it inferred,
every file it would write, and every tag it would remove from your HTML. Show
that plan to the user and get approval before re-running without --dry-run.
Useful flags: --source <file> · --name · --short-name · --description ·
--theme-color / --theme-color-dark / --bg-color · --start-url · --scope ·
--display · --site-url (needed for absolute og:image) · --og (render a
1200×630 share image) · --no-head (assets only) · --verify-only (audit, change
nothing — exits non-zero on failures, so it works in CI).
--base — required for any app not served at the domain root. Asset paths default to
the domain root, so an app at example.com/my-app/ would emit /icon-192.png and resolve
it against example.com, silently picking up whatever lives there instead. Pass
--base ./ for relative paths, which work at any mount point, or --base /my-app/ to
hard-code the subpath. --verify-only takes the same flag, so an audit resolves the icons
the way the manifest actually names them.
--lang and --dir are read from the <html> element when not passed, so an
<html lang="he" dir="rtl"> document produces an RTL manifest without being told.
How it works — env tools, never bundled libraries
Rasterising is the only step that needs anything installed. Manifest, <head>,
the padded SVG derivatives, .ico assembly and the whole verify pass are pure
Node built-ins and always run.
| Capability | Tools tried in order | If none found |
|---|---|---|
| SVG source → PNG | rsvg-convert · magick · inkscape · sharp (npx cache) |
manifest + <head> still written; PNG/ICO skipped, install hint printed |
| raster source → PNG | magick · sharp (npx cache) · sips (macOS) |
same |
| padding a raster source | magick only |
maskable + apple icons skipped, loud warning |
| flattening (removing alpha) | magick · rsvg-convert · inkscape |
icon written with its alpha channel and a warning saying so — never silently |
An SVG source is materially better: padding is baked into a generated derivative SVG, so every rasteriser yields a correct maskable icon.
What it produces
favicon.svg · favicon.ico (16/32/48) · favicon-96x96.png ·
apple-touch-icon.png (180, flattened onto a solid colour) ·
icon-192/512.png (purpose: any, transparent) ·
icon-maskable-192/512.png (separate files, mark inset to the central 80%) ·
optional og-image.png (--og, SVG source only — a raster source is warned and skipped,
not silently promised) · the manifest · the <head> block.
Manifest covers id, name, short_name, description, lang, dir,
start_url, scope, display, display_override, orientation, theme_color,
background_color, icons, categories, launch_handler, and screenshots
(auto-picked up from screenshot-*.png, with form_factor from their aspect ratio).
Head covers title, description, canonical, light/dark theme-color, icon links,
apple-touch-icon, apple-mobile-web-app-*, mobile-web-app-capable,
application-name, rel=manifest, Open Graph and Twitter card.
Two rules it enforces that hand-written manifests routinely break:
a maskable icon is never the same file as an any icon (declaring one file
"any maskable" gets its edges cropped on Android), and apple-touch-icon.png
never carries transparency (iOS paints transparent pixels black). The verify pass
fails on both, and only excuses an alpha channel when the run actually flattened the icon —
not merely because it produced it.
Agent duties
- Gate the write. The tool rewrites
<head>. It removes only tags it re-emits (plus stale icon rels) — an author'sog:locale,twitter:site,article:*or a hand-set canonical are left alone. Run--dry-run, show the removal list, get approval. Re-runs are idempotent: the block sits between sentinel comments and is replaced. Before the first edit the original HTML is copied to<file>.bakand the path is reported — tell the user, since that is their undo if the project is not under git. - Read stderr. Degradation and install hints land there; exit stays 0 when the manifest was written. Surface them and offer the install as a question — never install anything silently.
- Make the judgment calls the script won't. It infers
namefrom package.json andtheme_colorfrom the project's own CSS, and says so in anote. Confirm with the user: a realdescription(it shows in the install dialog), ashort_name≤ 12 chars, and the right brand colour. UseAskUserQuestionwhen more than one reading is plausible. - No mark? Author one first. Write a 512×512 SVG to the project, then pass
--source. It must be legible at 16px: one shape or 1–2 bold letters, no thin strokes, no fine detail, high contrast on both light and dark tab bars. - Offer screenshots. Without
screenshots[]Chrome shows the plain install prompt instead of the rich card. Capturing 1 wide + 1 narrow PNG into the static dir asscreenshot-*.pngis usually worth it — re-run to pick them up. - Frameworks that don't own an
index.html(Next, Nuxt, Astro, SvelteKit, Hugo, Jekyll) gethead-snippet.htmlwritten to the static dir instead; merge it into that framework's head or metadata export yourself. - Relay the verify block verbatim.
FAILlines mean the app will not install or will display wrong — fix them before calling the task done.
See references/manifest-spec.md for field-by-field detail, the platform size
matrix, and the traps behind each enforced rule.