Skip to main content

flexweg-favicon

flexweg-favicon turns one uploaded square image into the full favicon + PWA manifest cluster every modern browser expects, then injects the matching <link> tags into every published page.

It's must-use because every public site benefits from favicons — the cost of having it always-on is one <link> cluster per page, and the benefit is "your site looks like a real site in browser tabs and bookmark bars."

What it generates

From a single uploaded source image (PNG / JPG / WebP / SVG):

FilePath on FlexwegFormat
favicon-96x96.png/favicon/favicon-96x96.png96×96 PNG (browser tab)
apple-touch-icon.png/favicon/apple-touch-icon.png180×180 PNG with white background (iOS home screen)
web-app-manifest-192x192.png/favicon/web-app-manifest-192x192.png192×192 PNG, purpose: maskable
web-app-manifest-512x512.png/favicon/web-app-manifest-512x512.png512×512 PNG, purpose: maskable
favicon.ico/favicon/favicon.icoMulti-size (16/32/48) packed via pure-JS encoder
favicon.svg/favicon/favicon.svgPassthrough only when source is SVG
site.webmanifest/favicon/site.webmanifestPWA manifest (name, short_name, icons, theme_color, …)

All resizing happens client-side via createImageBitmap + canvas cover-crop. No server, no third-party API.

Settings

/settings/plugin/flexweg-favicon exposes:

Source tab

  • Upload image — square image, ideally 512×512 or larger. Drag-and-drop or click to pick.
  • Preview — shows the cropped result for each variant.
  • Generate + Upload — runs the full pipeline. ~5-10 s for a typical source.

PWA tab

  • App name — used as name in the manifest. Falls back to the site title.
  • Short nameshort_name; falls back to truncated app name.
  • Theme colour — browser chrome tint when the PWA is installed. Also injected as <meta name="theme-color">.
  • Background colour — splash screen background.
  • Display modestandalone (default), fullscreen, minimal-ui, browser.
  • Re-upload manifest — pushes only site.webmanifest (no source image required). Useful for tweaking PWA settings without re-running the resize pipeline.

How it hooks in

api.addFilter<string>("page.head.extra", (current, props) => {
const config = readConfig(props);
const tags = buildHeadTags(config);
return tags ? `${current}\n${tags}` : current;
});

The filter emits only the tags whose corresponding files were actually uploaded — so a partial install (e.g. raster source so no SVG variant) doesn't produce broken <link> references. Each URL is cache-busted via ?v=<uploadedAt> so changing the favicon invalidates browser caches on the next page load.

The plugin also registers a Regeneration target for Themes → Regenerate site → Favicon manifest that re-uploads site.webmanifest only. (The PNG/ICO/SVG variants would require the source image, which the plugin doesn't store — re-uploading them means going back to the Source tab and re-clicking Generate.)

Per-format flags

The plugin tracks which variants were successfully uploaded:

{
hasIco: true,
hasSvg: false, // source was raster
hasPng96: true,
hasAppleTouch: true,
hasManifest: true,
uploadedAt: 1735689600000,
}

Each flag gates one <link> tag in the output. So toggling SVG support doesn't require code changes — it follows from whether the source was an SVG.

Toggle

Disabling the plugin (in code, since it's MU and has no UI toggle) would stop emitting the <link> tags. Files at /favicon/ would persist on Flexweg until manually deleted.

When changes apply

  • Newly published pages include the new tags immediately (with the new ?v= query string).
  • Already-published pages keep their old <link> tags with the old ?v=. Browsers may serve cached favicons until the page is regenerated.

To roll out a favicon change site-wide: Themes → Regenerate site → All HTML pages.

Internal details

  • Source: src/mu-plugins/flexweg-favicon/
  • Hooks used: page.head.extra filter; registerRegenerationTarget
  • Storage path: /favicon/
  • Pure-JS ICO encoder: src/mu-plugins/flexweg-favicon/icoEncoder.ts
  • Translations: 7 locales

Continue