Changelog
All notable changes to this project are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[3.0.1] — 2026-08-30
Added
- MIT License — the project now carries an explicit
LICENSEfile and a matchinglicensefield inpackage.json, so GitHub, npm and dependency scanners report the terms instead of "no license" (which legally means all rights reserved). MIT was chosen over AGPL-3.0 because every dependency is already permissive — MIT, Apache-2.0 (sharp) and BlueOak-1.0.0 (lru-cache) — and because FaviconAPI is meant to be forked and self-hosted with as little friction as possible. - License section in the README and Getting started — records two things the license itself does not cover. The name FaviconAPI, the logo and the
faviconapi.comdomain fall outside the grant, so a public fork should run under its own name. And the icons are not ours to license: they are fetched from the upstream providers and catalogs at runtime, never shipped in this repository, so selfh.st icons, homarr dashboard-icons, LobeHub icons, SVGL, theSVG and the favicon providers each keep their own terms — worth checking before using the results commercially.
[3.0.0] — 2026-08-29
Added
- Preload domain database — a SQLite store at
PRELOAD_DB(default/cache/db/preload.sqlite) holding the domains worth warming, how often each is actually requested, and per-domain icon overrides. Newsrc/preloadStore.js(WAL +busy_timeout, schema versioned throughPRAGMA user_version) degrades silently: a missing or corrupt file means no counting and no overrides, never a failed favicon request. - Preload CLI —
scripts/manage-preload.jswithlist,add,enable,disable,set,remove,block,unblock,blocklist,seed-blocklist,import,export,recalcandstats, exposed asnpm run preload:list/preload:add/preload:enable/preload:disable/preload:set/preload:import/preload:export/preload:recalc. Manual actions mark a row as manually managed (y*/n*inlist) so no automated pass can flip it back. - Preload script —
--source db—scripts/preload-top-sites.jscan now take its domains from that database, highest rank first, skipping anything below--min-rank(defaultPRELOAD_MIN_RANK, normally 3). After the run it writeslast_preloaded_at,last_preload_result(standard_ok,v1_ok,sizes_ok),last_errorandfail_countback per domain, batched 25 at a time in a transaction, and reportsDB updated: N rows. A domain that keeps failing is switched off afterPRELOAD_AUTO_DISABLE_AFTERconsecutive failures — but only when it is not manually managed. - Popularity counting across every icon route —
GET /{domain}, the/scraper/...and provider aliases,/{domain}/json, the profile resolve route and/api/v1/faviconall feed therankcolumn. Counting is buffered per worker and flushed everyPRELOAD_HIT_FLUSH_MS, adding to the stored count so concurrent cluster workers cannot overwrite each other. Only HTTP 200 responses count, so typos and bot scans never create rows./admintraffic is excluded. - Per-visitor hit deduplication — the same visitor asking for the same domain counts once per
PRELOAD_HIT_DEDUPE_MS(default 60s). One search on the web UI fires 15-25 requests (the/jsondiscovery call plus one per provider card); without this it would outweigh a real embedded favicon by more than an order of magnitude. - Rolling popularity window — hits are stored per month in
preload_hits_monthly;manage-preload.js recalcsums the lastPRELOAD_RANK_MONTHS(default 3) intorankfor traffic-sourced domains, so old popularity fades instead of accumulating forever.preloadStore.hitsByPeriod()exposes those buckets so the effect ofrecalccan be judged against the traffic it was computed from. - Icon overrides (
alter_icon_url) —manage-preload.js set {domain} --icon-url https://...pins an exact image for a domain whose own favicon is unusable. Newsrc/iconOverride.jsresolves it from an in-memory map that is only re-read when the database file's mtime changes, so the lookup stays free in the request path. The override applies on all three pipelines —pickBest(), the sized scraper routes and/api/v1/favicon— so a preload run cannot cache the rejected icon on the routes that skip it. Responses reportX-Favicon-Source: override. The URL hash is part of the cache key, so changing the override takes effect immediately instead of afterDISK_CACHE_TTL. Onlyhttps:URLs are accepted. - Preload blocklist — exact (
example.com) or wildcard (*.gstatic.com) patterns. Blocking disables every already-listed domain that matches and reports how many; from then on the domain is never created by the hit counter, skipped on CSV import, refused byadd/enable, and left out of the preload selection. Unblocking does not re-enable those domains. Patterns live in their own table and are listed byblocklist, notlist.seed-blocklistloads the built-in CDN/DNS/ad-infrastructure set.addBlock()/removeBlock()drop the in-process pattern cache immediately in the writing process; other workers still pick the change up on their nextPRELOAD_OVERRIDE_RELOAD_MSwindow. - CSV import/export — round-trips the column order
id,domain,enabled,source,rank,last_preloaded_at,last_preload_result,last_error,alter_icon_urlso the list can be bulk-edited in a spreadsheet. On import theidcolumn is ignored (the domain is the key) and thelast_*columns are skipped, since the next preload run rewrites them. Imported domains are validated with the same hostname rules as the icon routes, so rows the API could never serve (*.test,*.local, scanner paths likewp-login.php) are counted as invalid instead of silently added. - Preload sets for worldwide top sites —
scripts/preload-top-sites.js(and the/adminPreload run form) can warm three named rankings besides the local database:--source similarweb(visited) — SimilarWeb most visited worldwide. The public page is the top 50 (including Adult-category sites); remaining slots up to--limitare filled from Backlinko so a top-100 request still returns 100.--source backlinko(popular) — Backlinko / Semrush most popular by monthly visits (top 100). NSFW is already removed by the publisher.--source dataforseo(pagerank) — most Google ranking keywords, the closest free stand-in for high PageRank / site authority. Live HTML is scraped on each run; July/August 2026 snapshots insrc/preloadSets.jsare used if the page layout changes or the request fails. Curated lists keep the published host (gemini.google.comis not collapsed togoogle.com).
--adult include|exclude— drop porn/adult domains from any source. SimilarWeb uses its Adult category; other sources use the built-in list insrc/adultDomains.js.--exclude-adult/--include-adultare aliases. The preload manager exposes the same choice as Adult / porn domains. Default isinclude, matching SimilarWeb's published ranking.- Preload manager at
/admin— a web interface for the preload database: search/filter the domain list, add domains, edit rank and icon override inline, enable/disable, delete, manage the blocklist (including the built-in service-domain seed), read popularity stats, recalculate ranks, import/export CSV and start a preload run with a live log. Every write goes throughsrc/preloadAdmin.js, the same layerscripts/manage-preload.jsuses, so the CLI and the web UI cannot drift apart on validation or CSV handling. - Username and password sign-in — the login form is two ordinary fields (
autocomplete="username"/"current-password"), so a password manager can store and fill them. Credentials come fromADMIN_USER(defaultadmin) andADMIN_PASSWORD_HASH, a self-describing scrypt hash —scrypt:N=16384,r=8,p=1:<salt>:<hash>, colon-separated and base64url becausedocker composeinterpolates$nameinside anenv_file— computed with node'scrypto. A wrong username costs a full hash computation too, and every failure returns the sameInvalid username or password.The session is a statelessHttpOnly; SameSite=StrictHMAC JWT (Secureover https) signed withADMIN_SESSION_SECRET, so it works across cluster workers with no shared session store. Verification uses an algorithm allowlist (alg: noneand asymmetric algorithms are rejected before the signature is computed),timingSafeEqualon the signature, andiss/aud/exp/nbfchecks. - Sliding sessions and Keep me signed in —
ADMIN_SESSION_TTL(default3600) is an idle timeout: any authenticated request past the halfway mark re-signs the cookie. The Keep me signed in checkbox switches that window toADMIN_REMEMBER_TTL(default30d, accepts30d/12h/45m/ seconds). - Credential CLI —
scripts/manage-admin.jswithsecret,password [--user name]andstatus, exposed asnpm run admin:secret/admin:password/admin:status.passwordasks twice without echoing (or reads piped stdin), enforces a 10-character minimum and prints theADMIN_USER/ADMIN_PASSWORD_HASHlines for.env; the password itself is never stored.secretalways prints a 512-bit key (HS512); existing 256-bit secrets keep verifying.statusreports both halves of the configuration, the scrypt parameters and both idle windows. - Preload runs from the browser —
POST /admin/api/preload/runstartsscripts/preload-top-sites.jsas a child process and tracks it through a lock, status and log file next to the preload database. The atomic (wx) lock keeps two workers from starting the same run, any worker can report progress, and a run whose process disappeared is reported asinterruptedwith the stale lock cleared. The run form has aTimeout (ms)field alongside limit, min rank and concurrency, passed as--timeoutand clamped to 1000–300000 (default 30000). - Cron command generator on Preload run — the manager builds a host crontab line from the current run options (schedule,
docker exec/docker compose exec -T, container name, log file, optionalrecalcfirst,--adult). Dry run is omitted so a scheduled job actually warms the cache. - Set contents preview on
/admin— View list on the Preload run tab opens a modal with a ranked table (#, domain, Adult) for the selected source. No icons; adult rows show an Adult badge. - Bulk domain actions on
/admin— the Domains table has a checkbox column (header ticks the whole page). Selected rows can be disabled, enabled or deleted in one go viaPOST /admin/api/domains/bulk. - Sort, state filter and column visibility on
/adminDomains — click a header to sort (remembered inlocalStorage). State is on or off only. Columns hides Rank, State, Source, Fails, Last preload or Icon override. SCRAPER_TOTAL_TIMEOUT(default15000ms) bounds one complete origin scrape, andSCRAPER_PAGE_TOTAL_TIMEOUT(default8000ms) the homepage-HTML step within it. Every step after the HTML talks to the same origin — up to 32 icon probes in batches ofSCRAPER_PROBE_BATCH_SIZE, each with its ownUPSTREAM_TIMEOUT, then the whole scrape again for thewww.variant. Against a host that accepts connections but never answers, the dedicated/scraper/route measured 88 seconds; it is now 8.1s, with the cheap fallbacks (catalogs, Google faviconV2) producing the icon./{domain}never showed this because there the scraper is raced against eleven fast providers — which is exactly why only the HTML Scraper card kept spinning in the UI.CACHE_STALE_RETENTION(default30days) turns the disk cache into stale-while-revalidate: past its TTL an entry is still served immediately while a background refresh runs, so a TTL expiry no longer lands on a visitor's clock.CACHE_REVALIDATE_COOLDOWN(default900s) is the minimum gap between two background refreshes of the same key, and refreshes are single-flighted per key. Raising the TTLs alone only moves the problem a week further out; this removes the cliff instead.CACHE_GC(defaulttrue) andCACHE_GC_INTERVAL(default6hours) add a periodic sweeper that deletes what actually expired.CACHE_SIZE_MBeviction only ever looked at the image cache, soscraper-discoveryand the v1 API directory grew without bound. One worker claims each sweep through a lock file.SCRAPER_PAGE_CACHE_TTL(default21600s) gives stored raw HTML — by far the largest consumer — its own short lifetime, independent of the icon lists inSCRAPER_ICONS_CACHE_TTL.
Changed
- Service/infra domain set moved to
src/serviceDomains.js—scripts/preload-top-sites.jsnow imports it instead of carrying its own copy, so the preload filter and the blocklist seed share one list. CSV parsing/serialization, icon-URL validation and the import loop live insrc/preloadAdmin.js. - Usage rank and list position are no longer the same number.
rankis rolling hit count for every source, so three real requests beat position 1 of a top-100 import. Ranking runs write list order tosource_rank(shown as#1on the Source column) and leave usage rank alone.--source dbwarms real traffic first; list-imported domains with no hits yet still fill remaining slots. Recalculate now rewrites every auto rank from the monthly hit buckets; a rank you type in/adminor--rankis pinned (rank_source = 'manual') and is not overwritten. DISK_CACHE_TTLandAPI_CACHE_TTLare now in days — defaults7and7(were86400/604800seconds). Values of 3600 or higher are still treated as seconds so existing.envfiles keep working; a startup warning tells you to switch to days./providersapi.cacheTtland CDNCache-Controlmax-age stay in seconds.- Default TTLs now match the retention the cache is actually designed around:
MEMORY_CACHE_TTL3600→86400,SCRAPER_ICONS_CACHE_TTL3600→604800. With a one-day disk TTL nearly every entry expired daily, so most requests were full upstream re-fetches and the one-hour memory cache only masked it briefly. CACHE_SIZE_MBrecommended default256→1024. The cap still only counts icon files in the cache root; discovery HTML and the v1 API directory are the sweeper's job. Loose non-cache files in the cache root (the preload SQLite database, CSV imports) are no longer indexed as cache entries, so eviction cannot delete them.- Homepage CDN-icon cards —
.env.exampleUI_APP_ICON_PROVIDERSdefaults toselfhst,dashboardicons,svgl,thesvg(LobeHub off unless you add it back). Unset still means all five catalogs. ADMIN_LOGIN_MAX_ATTEMPTSexample default is5(per IP per 15 minutes, counted per worker).- Getting Started wants a real
.env. Copy.env.exampleto.envand edit that file so secrets and defaults live in one gitignored place. The documented compose snippet usesenv_file: .env. docker-compose.ymldefaults to a localbuild: .. The GHCRimage:line is commented, sodocker compose up -dafter a clone builds this tree instead of pullinglatest.- README no longer duplicates every env var (that list lived in
.env.example). Quick start, Preload manager and Performance now point at the selfhosted guides; API key links go to API. - Selfhosted docs — one API page (image routes, JSON discovery, and v1); Tweaks is now Performance; sidebar Proxy → Reverse Proxy, Tools → Browser tools. Getting Started walks copy-to-
.env, Configuration, Preload manager and Browser tools before Routes. Old URLs 301:/docs/api-v1and/docs/api-reference→/docs/api,/docs/tweaks→/docs/performance. - Dockerfile sets
NPM_CONFIG_UPDATE_NOTIFIER=falsesodocker compose exec … npm run admin:password(and the other CLIs) is not buried under npm's "new version available" banner.
Fixed
- A domain with more than one A record no longer takes twenty seconds to scrape. Forcing IPv4 used
dns.lookup(all: false), which pins the first address and never tries the rest. Anycast front-ends routinely publish several:2fas.comresolves to188.114.96.0, which refuses connections here, and188.114.97.0, which works. DNS rotates the order, so the same domain was fast on one request and slow on the next — and when the dead address came first, each attempt cost ~2s ofECONNREFUSED. The homepage-HTML step alone tries ten combinations (domain +www., five header/HTTP-version variants) sequentially, which measured 20.7s for2fas.comwhile a browser loaded it instantly. The lookup now returns every IPv4 address and Node's Happy Eyeballs loop walks them, verified against a pinned[dead, good]order: 2058msECONNREFUSEDbefore, 634ms and HTTP 200 after.cloudflare.comwent from 4254ms to 986ms cold,tailscale.comfrom 10429ms to 3241ms. - The retry onto a second address was unreachable from the code that needed it most.
upstreamFetchalready fell back to the system resolver on a connect failure, but skipped it whenever the caller pinned a dispatcher — and every attempt in the scraper's HTML ladder pins one. The fallback now applies there too, onto a dispatcher that preserves the caller's HTTP/1-only choice. - A failed HTML fetch is no longer cached like a successful one.
{html: null}was written to the discovery cache for the full page TTL, so one network hiccup convinced every worker for hours that a site serves no HTML, silently degrading its icons to whatever besticon alone could find. Failures are now kept in memory only, forSCRAPER_PAGE_NEGATIVE_TTL(default 60s) — long enough to absorb the burst of requests one page load produces. A negative entry left on disk by an older version is ignored and deleted on read. /api/v1/faviconskipped the icon packs on a cold worker — the selfhst, dashboard-icons, lobehub, svgl and thesvg tiers resolve their slug through the synchronous alias lookup, which answers an empty string until the provider index is loaded. Nothing in the v1 path loaded it, so a freshly started worker requested URLs like…/selfhst/icons/png/.png, all five tiers 404'd, and domains those packs do cover came back as422 favicon_not_found.gatherCandidates()now awaits the indexes (shared between concurrent requests, TTL-cached as before). Most visible during a preload run, where every worker is cold: of 21 failures in a top-sites run, 6 — among themwordpress.com,spotify.com,indeed.comandamazon.co.uk— were this bug rather than a missing icon.- Preload run source attribution — warming Most popular / Most visited / Highest keyword rank (or a local
--domains-file) now writes those domains into the preload database before the API calls, withsourceset to that list (backlinko,similarweb,dataforseo,file) and list order insource_rank. Result writeback (last_preloaded_at, …) then runs for those rows the same way it already did for--source db. Preload-script requests (User-Agent: FaviconProxy-preload/1.0) are excluded from popularity counting so a run does not inflate traffic ranks. - Documentation — Copy on code blocks no longer leaves a blank strip.
.code-blockreserved extra top padding so the button would not cover the first line, which showed as an empty dark row. The button stays absolutely positioned in the corner; the spare space is now on the right instead. - Docs “On this page” was empty on Windows. Heading extraction split on
\nonly, so CRLF markdown (##lines ending in\r) never matched and every selfhosted guide except LF-saved Performance showed “No sections on this page.” Split is now\r?\n. Template placeholders are substituted via functions so a$hostin a nginx snippet cannot swallow.- [3.0.1] — 2026-08-30
- Added
- [3.0.0] — 2026-08-29
- Added
- Changed
- Fixed
- Security
- Notes
- [2.15.21] — 2026-08-22
- Fixed
- [2.15.20] — 2026-08-22
- Changed
- [2.15.19] — 2026-08-09
- Added
- Changed
- Added
- Changed
- Fixed
- [2.15.17] — 2026-08-07
- Added
- Changed
- [2.15.17] — 2026-08-07
- Fixed
- [2.15.16] — 2026-08-07
- Added
- Fixed
- Changed
- [2.15.15] — 2026-08-06
- Added
- [2.15.14] — 2026-08-06
- Fixed
- [2.15.13] — 2026-07-31
- Added
- [2.15.12] — 2026-07-30
- Fixed
- [2.15.11] — 2026-07-30
- Fixed
- [2.15.10] — 2026-07-30
- Fixed
- [2.15.9] — 2026-07-22
- Changed
- Added
- [2.15.8] — 2026-07-22
- Fixed
- [2.15.7] — 2026-07-22
- Changed
- Fixed
- [2.15.6] — 2026-07-21
- Fixed
- [2.15.5] — 2026-07-21
- Changed
- [2.15.4] — 2026-07-20
- Fixed
- [2.15.3] — 2026-07-20
- Fixed
- [2.15.2] — 2026-07-20
- Fixed
- [2.15.1] — 2026-07-19
- Changed
- [2.15.0] — 2026-07-19
- Added
- Changed
- Fixed
- [2.14.0] — 2026-07-15
- Added
- Changed
- Fixed
- [2.13.0] — 2026-07-14
- Added
- [2.12.0] — 2026-07-14
- Changed
- Fixed
- [2.11.1] — 2026-07-13
- Changed
- Added
- [2.11.0] — 2026-07-12
- Added
- Changed
- [2.10.2] — 2026-07-11
- Added
- Changed
- Fixed
- [2.10.1] — 2026-07-10
- Changed
- [2.10.0] — 2026-07-10
- Added
- [2.9.4] — 2026-07-09
- Changed
- Fixed
- [2.9.3] — 2026-07-09
- Added
- [2.9.2] — 2026-07-08
- Changed
- Fixed
- [2.9.1] — 2026-07-08
- Fixed
- [2.9.0] — 2026-07-08
- Added
- Changed
- Fixed
- [2.8.14] — 2026-07-08
- Changed
- Fixed
- [2.8.13] — 2026-07-08
- Fixed
- [2.8.12] — 2026-07-07
- Fixed
- [2.8.11] — 2026-07-07
- Changed
- Fixed
- [2.8.10] — 2026-07-07
- Added
- [2.8.9] — 2026-07-07
- Added
- Changed
- Fixed
- [2.8.8] — 2026-07-07
- Added
- Changed
- Fixed
- [2.8.7] — 2026-07-06
- Added
- Fixed
- [2.8.6] — 2026-07-05
- Added
- Changed
- [2.8.5] — 2026-07-05
- Added
- Changed
- [2.8.4] — 2026-07-05
- Changed
- Fixed
- [2.8.3] — 2026-07-05
- Fixed
- [2.8.2] — 2026-07-05
- Added
- Fixed
- [2.8.1] — 2026-07-05
- Added
- Fixed
- [2.8.0] — 2026-07-04
- Added
- Changed
- [2.7.6] — 2026-07-04
- Fixed
- [2.7.5] — 2026-07-04
- Added
- Changed
- [2.7.4] — 2026-07-02
- Changed
- [2.7.3] — 2026-06-30
- Changed
- Removed
- [2.7.2] — 2026-06-30
- Changed
- [2.7.1] — 2026-06-30
- Changed
- Fixed
- [2.7.0] — 2026-06-30
- Added
- [2.6.3] — 2026-06-29
- Fixed
- [2.6.2] — 2026-06-29
- Fixed
- [2.6.1] — 2026-06-29
- Added
- Changed
- [2.6.0] — 2026-06-29
- Changed
- Fixed
- [2.5.15] — 2026-06-28
- Changed
- [2.5.14] — 2026-06-28
- Fixed
- [2.5.13] — 2026-06-28
- Changed
- [2.5.12] — 2026-06-28
- Changed
- [2.5.11] — 2026-06-28
- Fixed
- [2.5.10] — 2026-06-28
- Changed
- [2.5.9] — 2026-06-28
- Fixed
- [2.5.8] — 2026-06-28
- Changed
- [2.5.7] — 2026-06-28
- Added
- [2.5.6] — 2026-06-28
- Added
- Fixed
- [2.5.5] — 2026-06-28
- Fixed
- [2.5.4] — 2026-06-28
- Changed
- Fixed
- [2.5.3] — 2026-06-28
- Changed
- [2.5.2] — 2026-06-28
- Changed
- [2.5.1] — 2026-06-28
- Fixed
- [2.5.0] — 2026-06-28
- Added
- Fixed
- [2.4.0] — 2026-06-26
- Added
- [2.3.3] — 2026-06-26
- Fixed
- [2.3.2] — 2026-06-26
- Fixed
- [2.3.1] — 2026-06-26
- Added
- Fixed
- Changed
- [2.3.0] — 2026-06-26
- Changed
- Added
- Fixed
- [2.2.0] — 2026-06-26
- Added
- Changed
- Fixed
- Internal
- [2.1.0] — 2026-06-25
- Added
- Changed
- Fixed
- [2.0.0] — 2026-06-25
- Added
- Changed
- Changed
- Fixed
- Internal
- [1.0.0] — Initial public release
- Added
Security
/adminrequires bothADMIN_SESSION_SECRET(at least 32 bytes) and a validADMIN_PASSWORD_HASH; with only one of the two the routes stay at 404 and the missing half is logged once at startup. Absurd scrypt parameters in a hand-edited hash (non-power-of-twoN, or one that would need more than 512 MB) are refused rather than allocated.- Admin responses drop the permissive CORS headers the icon routes carry and add
Cache-Control: no-store,X-Robots-Tag: noindex, nofollow,X-Frame-Options: DENYandReferrer-Policy: no-referrer;/adminis disallowed inrobots.txt. - Mutating requests must carry
X-Admin-Request: 1. Combined withSameSite=Strict, a third-party page cannot drive the admin API with the operator's cookie. - Sign-in attempts are throttled per IP (
ADMIN_LOGIN_MAX_ATTEMPTS, default 10 per 15 minutes in code,5in.env.example, counted per worker). - Rotating
ADMIN_SESSION_SECRETsigns everyone out at once. Changing onlyADMIN_PASSWORD_HASHblocks new sign-ins but leaves live sessions running until they idle out — relevant now that a remembered session can last 30 days.
Notes
- Breaking (Compose): copy
.env.exampleto.envbefore the firstdocker compose up -d.env_fileis read at container creation, sorestartkeeps the old environment. 2.15.x stacks that pointed Compose at.env.exampleneed that copy (and any secrets moved into.env) or Compose will not pick upADMIN_*and the new cache/scraper defaults. - Breaking (
/admin): the manager is off (404) until bothADMIN_SESSION_SECRETandADMIN_PASSWORD_HASHare set. Generate the hash withdocker compose exec favicon-api npm run admin:password. The legacy nameADMIN_JWT_SECRETis still read as a fallback for the session key and logs one deprecation warning at startup. - New environment variables:
PRELOAD_DB,PRELOAD_TRACK_HITS,PRELOAD_MIN_RANK,PRELOAD_AUTO_DISABLE_AFTER,PRELOAD_HIT_FLUSH_MS,PRELOAD_HIT_DEDUPE_MS,PRELOAD_RANK_MONTHS,PRELOAD_OVERRIDE_RELOAD_MS,ADMIN_USER,ADMIN_PASSWORD_HASH,ADMIN_SESSION_SECRET,ADMIN_SESSION_TTL,ADMIN_REMEMBER_TTL,ADMIN_LOGIN_MAX_ATTEMPTS,SCRAPER_TOTAL_TIMEOUT,SCRAPER_PAGE_TOTAL_TIMEOUT,SCRAPER_PAGE_NEGATIVE_TTL,SCRAPER_PAGE_CACHE_TTL,CACHE_STALE_RETENTION,CACHE_REVALIDATE_COOLDOWN,CACHE_GC,CACHE_GC_INTERVAL. All have working defaults except the two admin secrets, which keep/adminswitched off.CACHE_STALE_RETENTION=0disables serving stale and restores hard expiry. - The preload database deliberately lives in a
db/subdirectory of the cache volume.scanDiskCache()insrc/cache.jsindexes every loose file directly underCACHE_DIRandevictIfOverLimit()deletes the oldest ones onceCACHE_SIZE_MBis exceeded; directories are skipped. The same exposure applies to the existing/cache/api-keys.sqlite(including its-wal/-shmsidecars). - Databases created by unreleased 2.16–2.18 builds are migrated on first open: old list scores move to
source_rank, thenrankis refilled from recorded hits.
[2.15.21] — 2026-08-22
Fixed
- Workers no longer crash on
assert(!this.paused)— undici 7.28 (pulled in transitively by cheerio) threw an uncatchableAssertionErrorfrom the HTTP/1 parser when a peer closed the socket while the response body was paused. That killed the worker (Worker … died (code=1); respawning). undici is now a direct dependency at 7.29.0 (the#5360/#5474fix), unused response bodies are cancelled instead of left paused, and cheerio is overridden onto the same undici version. - Malformed percent-encoding returns 400 — scanner probes such as
/%c0made Express 5's router throwURIError: Failed to decode paramand dump a stack trace. Invalid paths are rejected before routing. - SVG rasterize — relative
xmlnsURIs — librsvg rejected Adobe leftovers such asxmlns:ns_sfw="ns_sfw;"(xmlns: URI … is not absolute), which surfaced asAPI normalize error/Scraper proxy errorfor domains likekroger.com. Invalid xmlns attributes are stripped before rasterizing. - logo.dev non-images — HTML/JSON error bodies are no longer passed to sharp (
Invalid magic bytes); they are treated as an upstream miss. - Fontconfig noise on Alpine — the runtime image now includes
fontconfigso SVG rasterization no longer logsCannot load default config file.
[2.15.20] — 2026-08-22
Changed
- Web UI — simplified homepage tip — the Tips block is now a single-line hint with an
mdi:magicicon instead of a titled “Tips” heading and bullet list. The example-URL “open in new tab” control usesmdi:external-link(wasline-md:external-link).
[2.15.19] — 2026-08-09
Added
- Web UI — icon-size selector in list view — the table/list view now has an Icon size slider (single thumb, one tick + tiny label per stop:
16/32/64/128/256/512) that sets the target size for every row. Click a tick or drag the thumb to choose; the selection persists inlocalStorageand drives the copied/opened URL and the Size column. It defaults to the largest standard size at or belowSCRAPER_MAX_ICON_SIZE(i.e.128). - Web UI — hide rows that cannot deliver the chosen size — a provider row is hidden entirely when it cannot serve the selected size: raster providers whose native icon is smaller (no upscaling) and any provider whose route rejects that size. The client mirrors the server-side per-route size validation (e.g.
svgl/thesvg/lobehubonly64/128/256,googleonly up to128,512only onbrandfetch), so the list no longer generates invalid URLs like/svgl/16/png/…. Vector (SVG) icons scale freely and always remain visible.
Changed
- Web UI — list view uses a fixed 16px thumbnail — every row reuses the icon the grid already loaded, rendered at a constant
16px, so rows stay compact regardless of the chosen size (the size only governs the target URL, Size column and row visibility). This replaces the previous behavior where the list auto-normalized each row to the preferred size (with a128→64→32→16fallback ladder) and showed a size badge next to the Size header.
Added
- Web UI — list view targets the preferred icon size — every provider row in the table/list view now normalizes to the configured preferred size (
SCRAPER_MAX_ICON_SIZE, default128) instead of showing each provider's raw delivered dimensions. When a provider cannot deliver that size, the row falls back to the largest smaller standard size (64→32→16); the copied/opened URL is retargeted to match the shown size. The preferred size is displayed as a small number badge next to the Size column header.
Changed
- Web UI — size filter removed from list view — the Size range slider (and its toolbar divider) is now hidden in the table/list view, and the list no longer applies the filter: all active providers are shown regardless of the slider position (rows are already normalized to the preferred size). The size filter continues to work unchanged in grid view.
Fixed
- Web UI — long provider titles are truncated on narrow screens — result card titles (e.g.
Faviconextractor.com) had no overflow handling, so on smaller screen resolutions the title ran into the Source / Download buttons in the card header. The.card-header h3(and its link) now truncate with an ellipsis (text-overflow: ellipsis) while the header buttons keep their space; the full name stays available via the link'shref.
[2.15.17] — 2026-08-07
Added
- Web UI — collapsible scraper size strip — the HTML Scraper card now shows only the standard sizes (
16,32,64,128,256,512) by default and tucks every other discovered size behind a+N more/− lesstoggle, keeping the strip tidy for domains that expose many odd icon sizes (e.g.76,120,144,180,192). The currently selected button always stays visible so the strip never shows an orphaned selection, and each search resets to the collapsed state.
Changed
- Web UI — oversize scraper sizes gated behind the Max filter — non-standard sizes larger than
512(e.g.1200×630og:imageshare cards) no longer count as "standard" and are omitted from the size strip unless the Size range filter's upper bound is set to Max (open-ended). When Max is selected they appear under the+N moretoggle. The strip rebuilds automatically as the upper bound crosses the Max stop.
[2.15.17] — 2026-08-07
Fixed
- Web UI — HTML Scraper shows a loader while loading — the initial (non-refresh) scraper preview was a "soft load" that kept the spinner hidden, so on a slow/cold scrape the card stayed blank with no visible loader. A fresh/blank scraper card now shows the spinner immediately until the first image appears; an already-visible preview is still kept on screen (no flash) while it upgrades to a larger discovered size.
[2.15.16] — 2026-08-07
Added
UI_SIZE_FILTER_MIN/UI_SIZE_FILTER_MAX— set the homepage Size range slider defaults (e.g.16–512). Applied when the visitor has no saved preference inlocalStorage. Allowed steps:0,16,32,64,128,180,256,512; max may also bemax/-1for the open-ended Max stop. Exposed onGET /providersassizeFilterMin/sizeFilterMax.
Fixed
- Disk cache — prefer cached scraper bytes over re-fetch — several paths re-scraped or blanked the UI even when icons were already on disk:
- Image cache only stored the display
buffer;originalBuffer/originalSvgBufferare now persisted as{key}.orig/{key}.origsvgfor the fullDISK_CACHE_TTL. DEFAULT_PROVIDER=scraperno longer invalidates warmbest_*entries whose provider isscraper-fallback:….- Unset
SCRAPER_ICONS_CACHE_TTLfollowsDISK_CACHE_TTL(was hard-coded 1 hour). buildSizedScraperIconresizes from a warmscraper_{domain}entry before running discovery; the Web UI keeps the visible preview while a preferred size loads (no spinner flash on e.g.yelp.com).
- Image cache only stored the display
- HTML scraper —
og:image/twitter:imagein discovery — widescreen Open Graph images (e.g. GitHub’s homepageog:imageat 1200×630) were dropped before/{domain}/json. Discovery now keeps probed meta images (source: og-image|twitter-image|…); the default/scraper/{domain}pick still ignores non-square metas when real favicons exist. Parses structuredog:image:width/:height/:type. UI prefers non-meta icons for the initial selection. - HTML scraper — Google fallback on the size strip — domains that only resolve via
scraper-fallback:googlev2(e.g.bbb.org) returnedicons: [], so the UI showed a single Fast-proxy button despite preloadsizes=6/6. Discovery now reflects the Google V2 fallback at its native size so all deliverable sizes are selectable. /scraper/{size}/…— max ismax, not 1024 — dropped the hard1…1024ceiling that rejected hi-res / og:image widths (e.g. 1200). Any positive integer is valid;maxserves the largest native source without applyingSCRAPER_MAX_ICON_SIZE. Invalid values returnUse a positive integer or max.
Changed
.env.examplecache defaults —DISK_CACHE_TTL/SCRAPER_ICONS_CACHE_TTL=604800(7 days),MEMORY_CACHE_TTL=86400,CACHE_SIZE_MB=512.
[2.15.15] — 2026-08-06
Added
- HTML scraper —
og:image/twitter:image/msapplication-TileImage—parseIconCandidatesFromHtmlnow also extracts these meta images as icon candidates (e.g.123accu.nl's Facebook logo). Near-square images only (aspect ≤ 1.4) so widescreen social share cards are skipped; real<link rel="icon">/ apple-touch / manifest icons still win once they are ≥ 128px. The v1 API adds anog-imagesource tier afterpngand before catalog fallbacks.
[2.15.14] — 2026-08-06
Fixed
- Web UI — scraper size strip no longer offers undeliverable 256/512 buttons —
scraperCanDownscaleTotreated every key in the JSONsizesmap as available, so domains whose largest source is e.g. 192×192 still showed 256 and 512 (raster is never upscaled; those clicks returned a smaller PNG). Buttons now require a discovered source ≥ the requested size (SVG/catalog icons that advertise 512 still unlock the full strip). Before discovery, only the Fast-proxy size (SCRAPER_MAX_ICON_SIZE) is offered. - Web UI — HTML Scraper defaults to the largest discovered icon — with
SCRAPER_MAX_ICON_SIZE=128, the card always opened on the capped Fast proxy (/scraper/{domain}→ 128×128) even when discovery found a larger native source (e.g. nu.nlandroid-chrome-192x192.png). The preview now selects the largest discovered icon first; Fast proxy remains available as an explicit size-strip button.
[2.15.13] — 2026-07-31
Added
- Web UI — open tip example URL in a new tab — after a domain or app-name search, the Tips example URL shows a
line-md:external-linkicon that opens the resolved/{domain-or-appname}URL in a new tab. The icon stays hidden while the placeholder is shown.
[2.15.12] — 2026-07-30
Fixed
- Underfilled SVG→PNG cache entries were never auto-invalidated —
isUnderfilledRasterskipped any entry whose source URL/content-type mentionedsvg, but best/scraper caches keep the.svgURL after converting to PNG. Broken Google product logos (tiny corner glyph) therefore stayed inbest_*until a manual?refresh=1. Detection now keys off the buffer only.
[2.15.11] — 2026-07-30
Fixed
/{domain}kept serving broken Google product logos after the SVG viewBox fix — best-pick caches (best_*) still held the underfilled corner-glyph PNGs, and?refresh=1was ignored on/{domain}(only/scraper/…honored it). Underfilled rasters (visible ink filling < 35% of a ≥64px canvas) are now rejected byisUnusableIcon, so stale entries self-heal on the next request.?refresh=1/?nocache=1on/{domain}also clears best-pick + scraper caches.
[2.15.10] — 2026-07-30
Fixed
- SVG → PNG — Google product logos rendered as a tiny icon in the corner — Google Workspace productlogos (Drive, Calendar, Meet, …) ship SVGs with
width/heightbut noviewBox.scaleSvgForRasterenlarged the canvas without scaling path coordinates, so/drive.google.comand/scraper/…showed a ~16px triangle in the corner of a black 128×128 frame. SVGs without aviewBoxnow getviewBox="0 0 {w} {h}"from their intrinsic size before the pixel size is pinned.
[2.15.9] — 2026-07-22
Changed
SCRAPER_MAX_ICON_SIZE— smaller icons via near-lossless palette PNG —encodeLosslessPngnow encodes both a truecolor and an indexed/palette PNG and keeps the smaller one. Real favicons are anti-aliased and carry 500-2000 colors, so a bit-exact palette is rare; instead the palette variant is accepted when its perceptual PSNR (alpha-premultiplied) vs. the source meetsSCRAPER_PNG_MIN_PSNR(default40dB ≈ visually lossless). Measured savings: github.com −25% (66.9 dB), netflix.com −26% (65.3 dB), reddit.com −53% (50.5 dB), all stillimage/png. This affects all scraper output throughcapScraperProxyOutput(/scraper/{domain}and serve-time re-cap).
Added
SCRAPER_PNG_PALETTE(defaulttrue) — enable/disable the near-lossless palette PNG pass for capped scraper output. Setfalseto force strict truecolor PNG.SCRAPER_PNG_MIN_PSNR(default40) — minimum perceptual PSNR (dB) the palette PNG must reach to be used instead of the truecolor PNG.0= always take the smaller file; raise toward strict lossless (50+ is visually indistinguishable,<35starts to show).
[2.15.8] — 2026-07-22
Fixed
- HTML Scraper — duplicate size labels in the size strip — after discovery the strip could show the same number twice when a native icon matched
SCRAPER_MAX_ICON_SIZE(e.g. two128buttons: discovered icon + Fast proxy). The Fast proxy keeps that slot; the redundant native button is omitted. Client-side discovery also collapses samewidth×heighticons to the highest-quality format (then largest bytes), matching the server-side rule from 2.15.5.
[2.15.7] — 2026-07-22
Changed
SCRAPER_MAX_ICON_SIZE— lossless PNG compression — when the cap is active,/scraper/{domain}(and serve-time re-cap) re-encodes output as PNG with max zlib compression (compressionLevel: 9) andensureAlpha, so file size shrinks without quality loss and transparency is preserved. Icons already within the pixel cap are re-encoded only when the result is smaller than the source. (adaptiveFilteringis left off — it can inflate flat icons.)- Web UI — Fast proxy tooltip shows file type and size — the lavender Fast proxy URL popup now shows a small
PNG · 5.9 KB-style badge in the top-right, fetched from the embed URL (/{domain}).
Fixed
/{domain}ignoredSCRAPER_MAX_ICON_SIZEfor cached scraper wins — best-pick served stalebest_*cache entries at full resolution (e.g. github.com at 512×512) while/scraper/{domain}correctly capped to 128. The best-pick route now re-appliescapScraperProxyOutputat serve time when the winning provider is scraper./scraper/{domain}skipped the size cap for SVG-sourced icons — after rasterizing an SVG to PNG,capScraperProxyOutputstill saw.svgin the original URL and returned early, so sites like github.com were served at 512×512 instead ofSCRAPER_MAX_ICON_SIZE. The skip now depends on the buffer/content-type actually being SVG.
[2.15.6] — 2026-07-21
Fixed
- SVG → PNG rasterization failed for large viewBoxes (e.g. SVGL Google Analytics) — icons whose SVG
viewBoxis thousands of units wide (Google Analytics ≈ 2200×2430) exceeded sharp’s input pixel limit when rasterized atdensity = size × 4, so/svgl/128/png/…and/svgl/256/png/…returned 500 and alternative-match thumbs stayed empty.rasterizeSvgToSizenow pins the SVG root to an explicit pixel size before rasterizing, then downscales to the requested size.
[2.15.5] — 2026-07-21
Changed
- HTML Scraper — duplicate sizes collapsed to the best icon — when discovery finds multiple icons with the same pixel dimensions (e.g. three
512×512URLs),fetchScraperAllIconskeeps only one per size. Preference is higher format quality (svg>png>webp>ico), then larger byte size. Applies to the size-strip UI,/{domain}/json, and sized/scraper/{size}/…source selection; existing memory/disk icon caches are cleaned on read.
[2.15.4] — 2026-07-20
Fixed
/scraper/512/…returned a 128×128 PNG whenSCRAPER_MAX_ICON_SIZE=128— catalog overrides/fallbacks (e.g.proton.me→ selfh.stproton) measured the capped proxy buffer and advertised128×128in discovery. Sized routes then skipped the full-res fetch (largestWidth < size) and fell through to the never-upscale path, so/scraper/512/png/proton.meserved 128px. Discovery now usesoriginalBuffer(native catalog size); SVG catalog hits advertise512; the sized fallthrough prefersoriginalBufferwhen present.- Sized scraper resize skipped for near-square sources (e.g. selfh.st
513×512) — downscale usedmin(width, height) > size, so a 513×512 catalog PNG was left unchanged for/scraper/512/…. It now uses the longer side, producing an exact512×512PNG.
[2.15.3] — 2026-07-20
Fixed
- Web UI — “Also include CDN icon lookups” no longer shows empty catalog cards for app-name searches — toggling the checkbox after a service search (e.g.
2fas) calledsetServiceCardsVisible(true), which resurfaced CDN provider cards (selfh.st, dashboardicons, SVGL, …) that had no catalog match. App-name searches already are CDN lookups, so the toggle is now a no-op for card visibility in that mode; Fetch still shows only providers with a match. Also skip the pre-show-all step beforestartServiceIconLoadsso unmatched empty shells do not flash on search.
[2.15.2] — 2026-07-20
Fixed
- HTML Scraper — sized icons now persist on disk —
GET /scraper/{size}/{domain}(and/png/variants) writescraper_{size}_{domain}(+.meta) viafetchWithCache, matching preload/tweaksdocs. Previously only the unsizedscraper_{domain}entry was stored; resized PNGs were recomputed on every request.?refresh=1/?nocache=1clears the unsized entry and all standard size files (16–512), plus the requested size if outside that set. - Web UI — size strip no longer capped by
SCRAPER_MAX_ICON_SIZE— that env only limits the unsized fast-proxy embed (/scraper/{domain}//{domain}). The HTML Scraper card now lists every size from/{domain}/json(sizes, including 256 and 512) so preloaded/cached sized files are reachable from the UI. - Web UI — standard size buttons available immediately — on search, the scraper card seeds 16 / 32 / 64 / 128 / 256 / 512 at once so clicks hit the sized image cache without waiting for
GET /{domain}/jsondiscovery. Discovery may still refine native icon variants afterward; the blocking “Discovering other icon sizes…” skeleton is gone.
[2.15.1] — 2026-07-19
Changed
- Web UI — smartphone header and navigation — on screens ≤700px the top nav collapses into a hamburger menu in the top-right (Home + About only; Tools and API are hidden). The homepage also hides the long intro paragraph on smartphones and places the site subtitle under the logo row. The Tips box is hidden on screens ≤1180px. Same hamburger behaviour on Home, API and Docs.
[2.15.0] — 2026-07-19
Added
- theSVG provider — new service-icon catalog backed by theSVG (6,400+ brand SVG icons; CDN docs). The catalog is loaded from
https://thesvg.org/api/registry.json(24 h TTL) and SVG assets are fetched fromhttps://thesvg.org/icons/{slug}/{variant}.svg. Canonical route:/thesvg/{size}/{ext}/{service}[?variant=color|light|dark], short alias:/ts/(plus legacy/ts/{service}[?size=][&variant=]). Native raster sizes 64, 128, 256; SVG is the default format (/thesvg/0/svg/{service}). APIcolormaps to theSVG'sdefaultvariant;light/darkare advertised only when the icon ships those files. Included in/services/resolve/:service(providers.thesvg),/{service}/jsonand/{domain}/json(endpoints.thesvg), the best-pick race, scraper catalog fallback (selfhst → dashboardicons → svgl → thesvg), the v1 API source priority,DEFAULT_PROVIDER=thesvg, custom profiles, and the Web UI (new theSVG card). UI_FAVICON_PROVIDERS/UI_APP_ICON_PROVIDERS— choose which homepage result cards are shown, with favicon providers and CDN icon catalogs as separate lists. Empty / unset = all. Exposed onGET /providersasfaviconProvidersandappIconProviders. The custom URL builder’s preferred/fallback dropdowns use the same lists. API routes are unchanged.- Documentation — Caddy optimizations — Proxy docs add a “Caddy optimizations” guide (compression, no global short
Cache-Control, HTTP/3/UDP 443, keep-alive buffers, optional edge cache) and the minimal Caddy snippet no longer overrides immutable/assetsand/cdn/faviconscache headers.
Changed
- Web UI — “App Icons” renamed to “CDN Icons” — section divider (grid + table), checkbox label (“Also include CDN icon lookups”), homepage/SEO copy, and empty-state messaging now say CDN Icons. Env/API keys (
UI_INCLUDE_APP_ICONS,UI_APP_ICON_PROVIDERS,includeAppIcons,appIconProviders) are unchanged. - Web UI — shared site header across Home, API and Docs — logo + FaviconAPI title sit in a left-aligned brand row with the top nav on the same line; the site-wide subtitle (“Get the highest resolution favicon…”) sits underneath. The homepage replaces the old centered providers line with a short intro (CDN catalogs, 14,000+ icons / 11,000+ unique with no overlap, still growing every day) plus a Tips box.
api.htmlanddocs.htmluse the same header layout; all three pages share a 1280px content width. - Preload script — DataForSEO only; CrUX and Tranco removed —
scripts/preload-top-sites.jsnow defaults to--source dataforseoand pulls domains exclusively from DataForSEO's free "Top 1000 Websites By Ranking Keywords" list (max ~1000). The--source crux,--source verified, and--source trancooptions are gone; onlydataforseoandfileremain.scripts/export-crux-top.jsis removed. - Preload script — new
--locationoption — DataForSEO exposes per-country rankings, so--locationaccepts a country name or numeric DataForSEO location ID (e.g.--location 0/Worldwide,--location Netherlands,--location "United States",--location 2528). Defaults toWorldwide(0). - Preload script — warms all scraper sizes by default — each domain now also hits
/scraper/{size}/{domain}for 16, 32, 64, 128, 256, 512 (512 added; previously opt-in via--sizesand capped at 256). Override with--sizes, or skip with--skip-sizes.
Fixed
- HTML scraper —
gmail.com(and other curated Google product aliases) now matchmail.google.com—gmail.comwas mapped to thegmailcatalog slug viadomainIconTags, so the scraper served the selfh.st PNG instead of the live gstaticproductlogosicon thatmail.google.comgets from the workspace marketing page.googleWorkspaceLogoFallbacknow also runs for curated Google icon-tag aliases (gmail,google,google-*), so both hostnames resolve to the same current Gmail product logo.
[2.14.0] — 2026-07-15
Added
- Preload script — Chrome UX Report (CrUX) as the default domain source —
scripts/preload-top-sites.jsnow defaults to--source crux, ranking domains by real Chrome page visits instead of Tranco's DNS/traffic aggregate. This naturally excludes pure-infrastructure domains (CDN/DNS/tracking backends) and dead sites. Because CrUX only ranks in coarse magnitude tiers (1000/10K/100K/1M) with random order within a tier, the script uses Tranco rank as a within-tier tiebreaker so the output is a genuine high→low popularity order (google, youtube, facebook, …). A new--sourceoption selectscrux(default),verified(Tranco order, kept only if the site also appears in CrUX),tranco(raw ranking), orfile. - Preload script — registrable-domain deduplication via the Public Suffix List — origins are collapsed to their eTLD+1 (fetched from publicsuffix.org at runtime, no new dependency), so
pt.xhamster.comandwww.xhamster.comboth becomexhamster.com, with correct handling of multi-level suffixes (go.id,co.uk,gov.co). Falls back to the last two labels if the list cannot be fetched. - Preload script — service/infra domain filtering — known CDN, DNS, cloud-backend and ad/tracking domains (e.g.
gstatic.com,akamaiedge.net,cloudfront.net,doubleclick.net) are dropped from the preload set by default; pass--no-filterto keep them. - Preload script —
--sizesoption — additionally warms extra icon sizes via/scraper/{size}/{domain}(comma-separated16,32,64,128,256; bare--sizeswarms all five). Previously only the standard best-pick and the 128×128 v1 PNG were cached; other sizes were generated on demand. Failed sizes are reported per size in the run summary.
Changed
- Best-pick cache key no longer carries a misleading size segment —
pickBest()stored its entry underbest_32_{domain}, but the32was only a cache-bucket label: the stored image is the winning provider's icon at its native/best resolution (scraper output capped atSCRAPER_MAX_ICON_SIZE, i.e. 128 px), not 32 px. The key is nowbest_{domain}(no size segment). Existingbest_32_*files are no longer read and expire via TTL/eviction.
Fixed
.microsoftbrand gTLD rejected as a scanner path (cloud.microsoft,static.microsoft) —microsoftwas listed inBLOCKED_FILE_EXTENSIONSinsrc/domainValidation.js, which is meant for bot-scan file extensions (.php,.env, …). Because.microsoftis a real ICANN brand TLD, valid domains such ascloud.microsoftwere rejected withinvalid_url: Could not parse url into a domain. Removedmicrosoftfrom the blocklist so these domains validate normally; genuine scanner extensions remain blocked.
[2.13.0] — 2026-07-14
Added
- HTML scraper — optional per-domain catalog source pin (
domainIconTagssource) — adomainIconTags.jsentry may now carry an optionalsourcefield ('selfhst','dashboardicons'or'svgl') to control which catalog serves its icon, e.g.{ domain: 'azure.microsoft.com', iconTag: 'microsoft-azure', source: 'dashboardicons' }. When set,fetchScraperCatalogFallbacktries that catalog first and then falls through to the remaining catalogs in the default order (selfhst → dashboardicons → svgl), so a missing strict match never drops the branded override; an unknown value is ignored. Entries without asourcekeep the existing default order unchanged. NewiconSourceForDomain()export indomainIconTags.js;listDomainIconTags()now includessourcewhen present.
[2.12.0] — 2026-07-14
Changed
- HTML scraper — curated
domainIconTagsmappings are now an authoritative override (azure.microsoft.com→ Azure logo) — previously an explicit domain→icon-tag mapping only acted as a fallback, used when the site's own favicon was too small (<MIN_SOURCE_SIZE). Sites that serve a large but generic favicon (e.g.azure.microsoft.comships the plain Microsoft four-square logo at 128 px, not the Azure icon) therefore never used their curated tag. Now, when a domain has an explicitdomainIconTagsentry, the scraper prefers the branded catalog icon (selfh.st / dashboardicons / svgl) over the site's own favicon. The one exception is when the HTML scrape already found a specific Google product logo (gstaticproductlogos, which is the exact, current brand icon — e.g.drive.google.com,meet.google.com), which is kept.fetchScraperandfetchScraperAllIconsapply the override consistently, so the served icon, the sized/scraper/{size}/…endpoints, and the/{domain}/jsonsize strip all reflect the same curated icon. - HTML scraper — catalog fallback now also tries SVGL — when a scraped site exposes no icon ≥
MIN_SOURCE_SIZE, the scraper falls back to a service-name catalog icon. The order wasselfhst → dashboardicons; it is nowselfhst → dashboardicons → svgl(infetchScraperCatalogFallback), so a strict SVGL slug match is used before dropping to the Google faviconV2 last resort. Matching stays strict (exact catalog slug / curated alias only).
Fixed
- ICO colours inverted for BMP-frame favicons (
azure.microsoft.com, and any.icousing BMP frames) — ICO frames were run through a BGRA→RGBA channel swap, butdecode-icoalready returns pixel data in RGBA order. The swap therefore inverted the red and blue channels, rendering e.g. the Microsoft four-square logo with blue/cyan/orange instead of red/blue/yellow. (PNG-frame ICOs were unaffected, which is why it went unnoticed.)icoFrameToPng/rasterizeIconow hand the frame's RGBA data to sharp unchanged. - HTML scraper — Google product domains (
meet.google.com,chat.google.com, …) — the scraper served an outdated catalog icon (e.g. the old multicolour Meet camera) instead of the product's real, current favicon. Fetchinghttps://meet.google.com/anonymously (as the scraper does) redirects to theworkspace.google.com/products/meet/marketing page, whose<link rel="icon">is only a 20×20 generic Google "G". That page's body, however, references every Google product's real, current logo on the gstaticproductlogosCDN (e.g.…/productlogos/meet_2026/v2/web/192px.svg). Two changes:parseIconCandidatesFromHtmlnow additionally extracts thoseproductlogosimage URLs and keeps the one whose product name matches the requested domain (derived from the leading domain label plus the resolved service slug, e.g.meet→meet,mail→gmail,drive.google.com→drive). The icon is scraped live, so it tracks Google's redesigns — no per-domain hardcoded URLs.fetchScraperForDomain's besticon branch now merges our own parsed HTML candidates (and their sized variants) into the candidate pool it ranks. Previously it selected only from the sidecar besticon's discoveries + manifest + static hints, so body-referenced icons (like the product logos above) were parsed but never selectable — the "best pick" fell through to thescraper-fallback:selfhst/dashboardiconscatalog even when the correct icon had been found.
- HTML scraper — Google product domains that require sign-in (
drive.google.com,docs.google.com,sheets.google.com,slides.google.com, …) — served an outdated catalog icon (e.g. the old sharp-triangle Drive logo) and, fordocs.google.com, a size strip limited to 16/32 px. Unlike meet/chat/calendar, these domains redirect anonymous requests to theaccounts.google.comlogin page, whose HTML exposes no product logo at all (only a 32 px generic favicon), so the scraper fell back to the staleselfhstcatalog logo. Fix: for Google product subdomains whose own HTML yields no matching product logo, the scraper now falls back to scraping the product'sworkspace.google.com/products/{slug}/marketing page — a reliable, public, live source that references every product's current logo on the gstaticproductlogosCDN. The slug is derived from the domain (leading label plus resolved service slug, e.g.drive→drive,mail→gmail), the fetch is cached and only runs when the site's own HTML did not already expose the logo, and no per-domain icon URLs are hardcoded. This also fixes thedocs.google.comsize strip, which now reflects the real scalable SVG logo (up to 512 px). New helpergoogleWorkspaceLogoFallbackis wired intofetchScraperForDomain,fetchScraperAllIcons, and the v1 API'sgatherCandidates. - HTML scraper — size strip did not reflect the served catalog fallback (
admin.google.com, and any bot-walled / sign-in-gated site with a catalog match) — when HTML discovery only surfaced a tiny favicon (e.g. a 32 pxaccounts.google.com/favicon.icobehind Google's abuse interstitial),fetchScraper()correctly served the largerselfhst/dashboardiconscatalog logo, but/{domain}/json'siconslist still contained only the tiny favicon. The UI caps the scraper size buttons at the largest discovered icon, so it offered only 16/32 px even though the served icon was 128 px.fetchScraperAllIconsnow injects the catalog-fallback icon into the discovered-icon list whenever discovery yields nothing ≥MIN_SOURCE_SIZEandSCRAPER_FALLBACKis enabled, keeping the advertised sizes consistent with the served icon (e.g.admin.google.comnow offers 16/32/64/128). - HTML scraper — wasted workspace fetches for Google product subdomains —
googleWorkspaceProductSlugsno longer tries the baregoogleslug (workspace.google.com/products/google/404s with a large body) or the redundant hyphenated service slug (e.g.google-drive), which never map to a workspace product page. Subdomains likeadmin.google.comnow attempt a single workspace page instead of three, reducing latency on the (already slow) bot-wall path.
[2.11.1] — 2026-07-13
Changed
- Self-hosted Iconify web component — the
iconify-iconscript is now served from the site's own origin (/vendor/iconify-icon-2.3.0.min.js) onindex.html,api.htmlanddocs.htmlinstead of the third-partycode.iconify.designCDN, and the now-unusedcode.iconify.designpreconnect was removed (theapi.iconify.designpreconnect for runtime icon data is kept). The component isdefer, so it blocksDOMContentLoaded; loading it from a third-party origin that advertises HTTP/3 could stall page load ~2 s on networks where QUIC (UDP/443) is blocked or unreliable, before falling back to TCP. Serving it same-origin (over the site's own HTTP/2) removes that third-party dependency from the critical render path.
Added
- Documentation — Cloudflare HTTP/3 — new "Cloudflare" section in the Proxy docs (and a cross-reference from Tweaks) explaining that, behind Cloudflare's proxy, HTTP/3 (with QUIC) should be disabled (
Zone → Speed → Settings → Protocol Optimization) to avoid the ~2 s QUIC-stall-then-TCP-fallback on affected client networks, with acurlcheck for thealt-svcheader.
[2.11.0] — 2026-07-12
Added
- Static asset extraction & caching — the inline
<style>and main<script>blocks ofindex.html,api.htmlanddocs.htmlare now split out at boot into separate, content-hashed, minified assets served from/assets/<hash>.cssand/assets/<hash>.jswithCache-Control: public, max-age=31536000, immutable. The HTML source files keep the blocks inline as the single source of truth; the extraction happens in-memory at startup (no files written to disk). Identical blocks shared between pages collapse to the same hashed URL (dedup), so shared CSS/JS is downloaded and cached once. New modulesrc/staticAssets.js; new dependencyesbuildfor minification (with a safe fallback to the un-minified blocks if unavailable). - Resource hints —
<link rel="preconnect">forcode.iconify.designandapi.iconify.designon all three pages so Iconify's runtime icon-data requests connect faster.
Changed
- Page delivery — CSS and JS are now loaded as separate cacheable resources (JS via
defer) instead of inline, so repeat visits and navigation between/,/apiand/docsskip re-downloading and re-parsing ~250 KB of markup. The homepage's main script shrank from ~250 KB inline to ~147 KB minified external. - Result images — provider/result
<img>on the homepage now useloading="lazy"anddecoding="async"(off-screen provider icons load only when scrolled into view); the site logo stays eager (LCP).
[2.10.2] — 2026-07-11
Added
- Browser Tools — bookmarklet preview duration — the Bookmarklet card now includes a Preview (sec) field (default 10 s, range 1–120). The value is stored in
localStorageand baked into the generated bookmarklet code on change. Re-drag FaviconAPI Copy after adjusting the setting so existing bookmarks pick up the new duration.
Changed
- Bookmarklet copy toast — preview duration increased from a hard-coded 5 s to the configurable value above; toast UI lives in the Bookmarklet tool card (alongside FaviconAPI Copy) with the hint below the controls.
- CORS middleware — OPTIONS preflight now echoes requested headers and responds with
Access-Control-Allow-Private-Network: truewhen applicable; cross-origin GET responses also send that header so browsers can reach self-hosted instances from public HTTPS pages when permitted. - Favicon responses —
Cross-Origin-Resource-Policy: cross-originon proxied icon bodies so embedded previews are not blocked by default CORP behaviour.
Fixed
- Bookmarklet icon preview — the toast no longer relies on
onerroralone (CSP blocks often fail silently). It shows the page's own favicon immediately, then upgrades to the FaviconAPI icon viafetch()+ blob when the host page allows cross-origin requests. On strict CSP sites (e.g. formula1.com) the page favicon remains as preview; the copied URL is still always the FaviconAPI link. - Bookmarklet preview fetch — removed the
response.okguard so 404 placeholder PNGs from the API can still be shown viafetch()+ blob when CORS permits.
[2.10.1] — 2026-07-10
Changed
- Web UI — Browser Tools icons — replaced emoji/SVG icons in the offcanvas panel with Material Symbols via Iconify:
search,link-2,bookmarkandtoggle-offon the four tool card titles; matching icons on the Search from browser, Build custom URL and FaviconAPI Copy buttons. Applied onindex.html,api.htmlanddocs.html. Iconify script added toapi.htmlanddocs.html. - Web UI — top navigation font size — increased
font-sizefrom0.85remto0.95remfor nav links, the About button and the Tools button onindex.html,api.htmlanddocs.html.
[2.10.0] — 2026-07-10
Added
- Browser Tools — Offline search page — new tool in the Browser Tools panel that generates a downloadable, self-contained HTML file for looking up favicons from your own computer. The file embeds your FaviconAPI server URL and profile configuration (preferred provider, fallbacks, minimum size) and requires no installation — just open it in any browser. Features:
- Preferred provider shown prominently with the icon at profile size, fallback providers in a grid below.
- Size buttons on the HTML scraper card (16 / 32 / 64 / 128 px) to preview the icon at different resolutions with actual dimensions displayed.
- Three actions per provider: Server (copy proxy URL), Source (copy upstream URL, domain-based providers only), Download (save icon to disk).
- Download all button to batch-download icons from every provider in the chain.
- Click any icon to copy its server URL to the clipboard.
- Catalog providers (selfhst, Dashboard Icons, LobeHub, SVGL) automatically strip the domain to a slug when the query contains a dot (e.g.
plex.tv→plex). - Umami analytics tag is embedded when
UI_ANALYTICS_SCRIPT_SRCandUI_ANALYTICS_WEBSITE_IDare configured.
- CORS headers — all responses now include
Access-Control-Allow-Origin: *so the offline search page (and other cross-origin clients) can fetch icons viafetch(). - Documentation — added "Offline search page" section to the Tools docs page.
[2.9.4] — 2026-07-09
Changed
- Web UI — "Build custom URL" offcanvas opening — clicking the toolbar button now opens the offcanvas panel at its default 400 px width instead of first sliding in at 400 px and then expanding to 720 px with a visible delay. The panel only expands to 720 px when a tool section ("Search from browser" / "Build custom URL") is opened inside the panel, matching the behaviour of the Tools menu item.
Fixed
- Web UI — offcanvas header docs link — moved the
?documentation link from the "Custom URL" tool card to the offcanvas header next to "Browser tools", linking to/docs/tools. The link is now visible on all three pages (index.html,api.html,docs.html).
[2.9.3] — 2026-07-09
Added
- Web UI — size filter "Max" step — the maximum-size slider now has a Max step beyond 512 px. Dragging the right handle all the way to the end removes the upper size limit entirely, showing all discovered icons regardless of how large they are. The label updates to e.g.
16px – Maxwhen a minimum is set, orAll sizeswhen no lower bound is active either. The minimum-size slider still tops out at 512 px (a minimum of "Max" makes no sense).
[2.9.2] — 2026-07-08
Changed
- Web UI — HTML Scraper remembers discovery per domain — the size-discovery result from
GET /{domain}/jsonis now cached in-page (keyed by domain). Repeat searches for a domain already looked up in the same session render instantly from memory: noGET /{domain}/jsonround-trip and no "Discovering other icon sizes…" loader. The scraper Refresh button (and?refresh=1) bypasses the cache and re-runs discovery, overwriting the stored result. - HTML Scraper — discovery disk cache on by default —
SCRAPER_DISK_CACHEnow defaults to true (unset/empty enables it; only an explicitfalse/0/no/offdisables it). Scraper discovery (homepage HTML, merged icon list, besticon JSON, manifest parses, icon-probe metadata) is persisted underSCRAPER_DISK_CACHE_DIR(/cache/scraper-discovery), soGET /{domain}/jsonandGET /scraper/{domain}read from disk instead of re-scraping on every request. The cache now survives container restarts and is shared across cluster workers (each worker previously re-scraped a domain the first time it saw it). Updated.env.example. - Favicon
Cache-Controlgainsimmutable— proxy favicon responses now sendpublic, max-age=86400, immutableso browsers reuse the cached icon on repeat views without a revalidation round-trip.
Fixed
- Web UI — Firefox loaded every icon twice — provider image loaders created a detached
new Image()withcrossOrigin = 'anonymous'and then copied itssrconto the visible<img>(which had nocrossOrigin). Firefox partitions its HTTP cache by CORS mode, so the visible<img>missed the bytes the loader had just fetched and re-downloaded every icon — roughly doubling scraper/provider requests (worse than Chrome, which reused them).crossOriginwas unnecessary here — all icons are served from the same-origin proxy, there is no<canvas>use, and downloads go throughfetch()— so it was removed from all image loaders. Each icon is now fetched once and repeat searches reuse the browser cache.
[2.9.1] — 2026-07-08
Fixed
- Web UI — app-icon catalog controls — SVGL card format/variant buttons now share the same
size-buttons-toolbarwrapper as the other service-icon cards, so PNG/SVG and Color stay on one row. - Web UI — catalog size buttons ignored clicks — with the default size filter active,
applySizeFilter()reset catalog PNG sizes to the auto-picked raster step (e.g. 256) after every preview load, so manual 128/64/32/16 selections appeared dead. Manual sizes within the active filter are now preserved (catalogHasStableRasterSize). - Light/dark service-icon variants — API/UI variants again match the preview semantics (Light = pale icon on a dark background, Dark = dark icon on a light background). Corrected
pngVariantSuffix()for selfh.st and dashboardicons.com, LobeHub theme folder mapping, and SVGL route selection. Web UI preview backgrounds were already correct; only the upstream asset choice was wrong. - selfh.st light/dark PNG previews — upstream
-light/-darkPNGs often bake in mismatched circular backgrounds (e.g.github-light.png). Light/dark requests now prefer the monochrome SVG variant and rasterize server-side; variant availability probes and/{service}/jsonsource URLs follow the SVG assets. Cache keys bumped (light_v5/dark_v5). - Web UI — Light/Dark buttons missing on catalog cards — variant probes appended
?probe=to proxy URLs that already carried?variant=…, producing invalid URLs (HTTP 400) so Light/Dark toggles never appeared even when the API listed the variants. Probes and catalog image loads now append cache-busting params with&when a query string is already present.
[2.9.0] — 2026-07-08
Added
scraperMaxIconSizeonGET /providers— exposesSCRAPER_MAX_ICON_SIZEto the Web UI so the HTML Scraper fast-proxy size button (e.g. 128) can render before/{domain}/jsonfinishes.
Changed
- Web UI — HTML Scraper parallel preview —
GET /scraper/{domain}starts alongside/{domain}/jsoninstead of waiting for the discovery response. Repeat visits can use browser HTTP cache: scraper image URLs are stable on normal searches;?_=timestampcache busting applies only on explicit refresh (refreshScraper()/?refresh=1). - Web UI — HTML Scraper progressive size strip — when
SCRAPER_MAX_ICON_SIZEis set, the fast-proxy button appears immediately next to a “Discovering other icon sizes…” loader (spinner + skeleton pills) while JSON discovery runs. The preview uses soft-load so the capped proxy icon can appear without hiding it behind the main favicon spinner.
Fixed
- HTML Scraper — icon probes bypassed disk cache —
fetchScraperAsset(used when the scraper probes candidate icon URLs during discovery) always fetched from the internet, even when the same bytes were already on disk from/s-asset, sized scraper routes, or an earlier probe. It now readsasset-raw_*andasset-v2_*entries fromCACHE_DIRbefore going upstream, and writes new fetches back asasset-raw_*. Repeat scrapes and container restarts with a persistent/cachevolume no longer re-download every candidate icon.
[2.8.14] — 2026-07-08
Changed
- Web UI — size filter default — the dual-range slider now starts at 16–512px on first visit (no saved preference in
localStorage). - Web UI — size filter on tablets — the dual-range slider and download-size buttons (
.download-toolbar-left) are visible on tablets (≥800px). The left toolbar cluster is hidden at ≤799px (was ≤700px); smartphones and narrow tablets no longer show the size filter or download-size strip.
Fixed
HTML Scraper — SVG-only sites (e.g. hosthatch.com) — domains with only an SVG
<link rel="icon">were missing from/{domain}/jsonicons and sized routes (/scraper/256/png/…,/scraper/512/png/…) could return blurry upscaled PNGs whenSCRAPER_MAX_ICON_SIZEcapped the default cache at 128px. The scraper now probes HTML page link candidates (including sized variants), treats SVG sources as 512×512 for discovery/ranking, rasterizes SVG directly at the requested size, and only downscales raster icons when the source is larger than the target. Empty icon lists are no longer treated as a valid disk/memory cache hit.Web UI — HTML Scraper card hidden with size filter — with the default 16–256px filter active, the scraper card could vanish from the grid for SVG-only domains:
writeCardMetaignoredisVector: falsewhen the source URL ended in.svg, so the card was classified as vector and hidden whenever a maximum size was set. ExplicitisVector: falseis now respected; scraper cards are never treated as vector; the scraper card stays visible in the grid while size buttons outside the filter range are disabled. The Fast proxy button (/{domain}, e.g. 128px withSCRAPER_MAX_ICON_SIZE=128) stays clickable; proxy and proxy-size selections are preserved when the size slider changes.
[2.8.13] — 2026-07-08
Fixed
- Web UI — size filter disabled out-of-range size buttons — with an active size filter (e.g. the default 16–256px), clicking a provider size button outside the range (e.g. 512 on the HTML Scraper, or 64 when the minimum is 128) loaded that size and the card was hidden by the filter (
size-filter-hidden), which looked like the preview frame jumping away or vanishing. Size buttons on HTML Scraper (proxy and discovered sizes), Google, FaviconKit, favicon.run, Twenty-icons, Brandfetch, and app-icon catalogs (selfh.st, dashboardicons.com, LobeHub, SVGL) are now disabled when they fall outside the active min/max;switch*Sizehandlers ignore those sizes as a safeguard. Buttons re-enable when the slider range includes them again.
[2.8.12] — 2026-07-07
Fixed
- Best-pick — stale
bestcache ignoredDEFAULT_PROVIDER— after upgrading to 2.8.11,GET /{domain}could keep serving an old CDN fallback (e.g. DuckDuckGo's generic Google G forcalendar.google.com) whileGET /scraper/{domain}showed the correct product icon. Thebestcache layer stores whichever provider won an earlier race; CDN icons are valid raster/ICO responses, so they were not invalidated by placeholder detection. WhenDEFAULT_PROVIDERis set, a cachedbestentry from a different provider is now discarded and the default provider is fetched again. Fixes the frontend vs/{domain}mismatch when the HTML Scraper card preview (scraper) and the short embed URL (best-pick) diverged.
[2.8.11] — 2026-07-07
Changed
DEFAULT_PROVIDERon/{domain}— when set, that provider now runs exclusively first; the fallback race only starts after it fails (null, empty, or placeholder). Previously it only received a head-start (PICK_HEAD_START_MS, default 150ms), so fast CDN placeholders (e.g. ryanjc's generic globe SVG) could still win over a slower scraper even withDEFAULT_PROVIDER=scraper.PICK_HEAD_START_MSnow applies only whenDEFAULT_PROVIDERis unset, or to the first fallback after the default fails. Docs updated in.env.example, Getting Started, and Tweaks §7.
Fixed
- Best-pick — ryanjc placeholder on private / unreachable hosts —
GET /{domain}could return favicon.ryanjc.com's generic tabler earth SVG (often rendered as a black globe) whileGET /scraper/{domain}correctly served the site's own icon. The placeholder is now detected inisUnusableIcon(same pattern as favicon.so and Vemetric placeholders) and rejected in the best-pick race and per-provider caches. Stalebestcache entries that hold the placeholder are invalidated on the next request.
[2.8.10] — 2026-07-07
Added
Preload CLI — Tranco top sites —
scripts/preload-top-sites.jswarms the favicon cache for the world's most visited websites. By default it downloads the latest Tranco top 500 and, per domain, callsGET /{domain}(best-pick provider cache) andGET /api/v1/favicon?url=…(normalized 128×128 PNG underAPI_CACHE_DIR). Options:--limit,--concurrency,--api-key,--domains-file,--skip-standard,--skip-v1,--timeout,--dry-run. Exposed asnpm run preload:top-sites(script is included in the Docker image underscripts/).Selfhosted docs — preload guide — Tweaks §10 Preload popular sites after deploy: Tranco source, Docker-only usage (
docker compose exec favicon-api …), options table, and expectations (duration, failures, load). API v1 CDN section links to the same guide.
[2.8.9] — 2026-07-07
Added
- Web UI — results table view — switch between the existing card grid and a compact table layout from the download toolbar (grid / table toggle). The choice is remembered in
localStorage(faviconapi-results-view). Columns: Icon, Provider, Extension (PNG/SVG toggle for catalog providers that support both), Size, Link, Source, and Download. The table stays in sync with the grid: size filter, loading skeletons, app-icons section divider, extension switches, and per-row download all mirror card behaviour. Copy URL from the icon, Size, Link (dedicated copy button), or Source column; the link text itself still opens in a new tab.
Changed
- Web UI — download toolbar on small screens — the size filter (dual-range slider and label) is desktop-only (hidden below 1210px, smartphones and tablets). On smartphones (≤700px), the grid/table toggle, Build custom URL button, and the left toolbar cluster are hidden; results always use grid view on that breakpoint (table preference in
localStorageis kept for wider screens). - Web UI — table Download column — download buttons are centered in the column.
- Web UI — placeholder results grid once per browser — the skeleton preview is remembered in
localStorage(faviconapi-placeholder-seen) instead ofsessionStorage, so reopening a closed tab or refreshing no longer shows the skeleton again. LegacysessionStorageflags are migrated on first load.
Fixed
- Web UI — placeholder on tab restore — restoring a closed tab from the back-forward cache no longer leaves the skeleton grid visible when the preview was already shown; a
pageshowhandler clears stale placeholder state. - Web UI — placeholder vanishing on first load — the skeleton preview disappeared as soon as
GET /providersfinished because the follow-up refresh treated the grid as stale; the secondshowPlaceholderResults()call now only updates provider visibility instead of hiding the grid.
[2.8.8] — 2026-07-07
Added
- Web UI — size filter — filter the results grid by icon dimensions with a dual-range slider (left thumb = minimum, right thumb = maximum; snap to 16–512px). SVG icons always pass the minimum (vector, unlimited resolution) but are hidden when a maximum is set. App-icon catalogs that offer SVG (selfh.st, dashboardicons.com, LobeHub, SVGL) load as SVG when only a minimum is set, or as PNG at the largest fitting size when a maximum applies; the filter also re-applies if a catalog initially loads the wrong format. Filter choices are remembered in
localStoragefor the next visit. Download-all, the no-icon notice, and the app-icons divider respect the active filter.
Changed
- Web UI — size filter default — the dual-range slider now starts at 16–256px on first visit (no saved preference in
localStorage).
Fixed
- Web UI — size filter hid SVG catalog providers — selfh.st, dashboardicons.com, LobeHub, and SVGL could disappear when a size filter was active: minimum-only filters left catalogs on 128px PNG instead of SVG; maximum filters (e.g. ≤512px) requested unsupported 512px PNG (HTTP 400) or kept SVGL on SVG (hidden because SVG exceeds any max); min+max ranges above native PNG size (e.g. 512–512px) left catalogs on too-small PNG with no SVG fallback. Catalog cards now re-sync format and reload when the slider changes, switch to SVG when a minimum is set (including combined min+max when no native raster fits), switch to PNG at the largest native raster size when only a maximum applies, and no longer re-apply the PNG size control after preparing SVG on a fresh search. HTML Scraper and native-sized provider cards (Google, FaviconKit, etc.) now auto-select the largest size that fits the active filter instead of always defaulting to 128px — including scraper-discovered icons (e.g. GitHub's 512px app icon) and SVG-only sites rasterized via proxy sizes.
[2.8.7] — 2026-07-06
Added
favicon.ryanjc.com provider — new domain favicon provider backed by api.favicon.ryanjc.com. Upstream accepts a domain via
?url=. Canonical route:/ryanjc/{size}/{domain}, short alias:/rj/{size}/{domain}. Resized server-side. Included in/{domain}/jsondiscovery, the best-pick race (GET /:domain),DEFAULT_PROVIDERoptions, custom profile URLs, and the Web UI.twenty-icons.com provider — new domain favicon provider backed by twenty-icons.com. Upstream supports native sizes (16, 32, 64, 128, 180, 192) via
/{domain}/{size}path. Canonical route:/twentyicons/{size}/{domain}, short alias:/ti/{size}/{domain}. Included in/{domain}/jsondiscovery, the best-pick race (GET /:domain),DEFAULT_PROVIDERoptions, custom profile URLs, and the Web UI with size-button strip. Size buttons are hidden when the upstream does not return an icon at that native resolution; upscaled responses (correct dimensions but no extra detail vs. the next smaller size, e.g. reddit.com/128 ≈ soft 64px) are rejected server-side so only genuinely sharper sizes are offered.
Fixed
- Upstream fetch — Cloudflare multi-A fallback — forced-IPv4 upstream requests used only the first DNS A record (
dns.lookupwithall: false). Some Cloudflare anycast nodes refuse connections on certain routes while a sibling address works, which broke providers such as favicon.ryanjc.com (ECONNREFUSED).upstreamFetchnow retries with the system resolver when the pinned IPv4 attempt fails to connect (ECONNREFUSED,ETIMEDOUT,ENETUNREACH,EHOSTUNREACH).
[2.8.6] — 2026-07-05
Added
- Selfhosted docs — Changelog — root
CHANGELOG.mdrendered at/docs/changelogwith a sidebar nav entry (src/docsRender.js); single source of truth with the repo changelog (Keep a Changelog). The CHANGELOG link on Getting Started navigates to that page.
Changed
- Selfhosted docs — Getting Started source links — GitHub repo and version/changelog grouped under a Source section; version shown as
FaviconAPI v{version}via3.0.1placeholder substituted frompackage.json. - Docker image —
CHANGELOG.mdincluded in the build (.dockerignoreexception +DockerfileCOPY) so/docs/changelogworks in containers. - GitHub repository renamed —
R0GGER/maflplus-favicon-apiis nowR0GGER/favicon-api; selfhosted docs and README links updated accordingly. - GHCR image — published images are now
ghcr.io/r0gger/favicon-api:latest(wasghcr.io/r0gger/maflplus-favicon-api:latest). The Docker Publish workflow still uses${{ github.repository }}, so no workflow edit is required after a rename. - Docker Compose service — compose service and
container_namerenamed frommaflplus-favicon-apitofavicon-api; updatedocker compose execcommands and any external references that used the old service name.
[2.8.5] — 2026-07-05
Added
- Web UI — About on small screens — on viewports below 1210px (smartphones and tablets), the top nav shows About instead of Docs. Tapping it opens a modal with the Why FaviconAPI? intro from the getting-started guide. On
/docs/*at the same breakpoint, the full documentation layout is hidden and only that About text is shown, with a note that full docs are best read on a desktop.
Changed
- Web UI — Docs nav desktop-only — the Docs link is visible from 1210px upward only; About is hidden on desktop.
[2.8.4] — 2026-07-05
Changed
- Web UI — placeholder results grid only on first visit — the skeleton preview appears once per browser session on the first homepage load; after a search or reset it stays hidden, including on refresh (
sessionStorage). Reset clears the grid instead of restoring the skeleton.
Fixed
- Web UI — app icon skeletons in placeholder mode — when Also include app icon lookups is enabled (via server config), the App Icons divider and service-icon skeleton cards now appear correctly after
GET /providersloads; fixed a race where the placeholder rendered before the checkbox state was applied.
[2.8.3] — 2026-07-05
Fixed
- logo.dev monogram placeholders rejected — upstream requests now use Logo.dev's
fallback=404parameter so unknown domains return 404 instead of a generated letter monogram. The/logodev/{size}/{domain}route and Web UI card return 502 / hide the card when no real logo exists; cache keys were bumped so stale monogram entries are not reused.
[2.8.2] — 2026-07-05
Added
- Web UI — "No icon found" notice — when every provider (and app-icon catalog, if enabled) fails to load an icon, a banner appears above the results grid stating that no favicon was found, with a preview of the black placeholder and the direct
/{domain}URL.
Fixed
- Web UI — stale icons after a new search — provider cards are reset at the start of each domain lookup, and a failed load no longer keeps the previous domain's image (e.g. GitHub icons after searching
hibbit.net). The "size not available" fallback still applies only when switching sizes within the same domain.
[2.8.1] — 2026-07-05
Added
- Not-found placeholder icon — when all providers fail (
GET /{domain}, custom profile URLs, service lookups), a solid black PNG placeholder is returned (HTTP 404) instead of a transparent 1×1 pixel. Sized to the profile minimum or 32px for best-pick. Asset:src/assets/not-found.png; helper:src/notFoundPlaceholder.js.
Fixed
- Favicon.so / Vemetric generic placeholders rejected — Favicon.so's SVG code-bracket icon and Vemetric's "world-question" fallback are no longer treated as real favicons in best-pick, profile resolve, or dedicated provider routes; the chain continues to the next source or the not-found placeholder.
- Not-found responses are not cached — placeholder results (
notFound: true) are never written to the disk/memory cache; staleprovider: noneentries are evicted on read. HTTP responses useCache-Control: no-storeso browsers and CDNs do not cache 404 placeholders (found icons still usemax-age=86400).
[2.8.0] — 2026-07-04
Added
- Selfhosted docs (
/docs) — markdown guides fromsrc/docs-content/rendered at runtime viamarked(src/docsRender.js,src/public/docs.html). Pages: Getting Started, Tweaks, API reference, API v1, Proxy, and Tools. Three-column layout with sidebar nav, per-page table of contents, code copy buttons, and responsive tables./docsredirects (301) to/docs/getting-started. Toggle withUI_ENABLE_DOCS(default on); when off, routes return 404 and the Docs nav link is hidden on/and/api.GET /providersexposesdocsEnabled. - Configurable analytics — optional Umami-compatible tracking snippet injected on
/,/api, and/docsviaUI_ANALYTICS_SCRIPT_SRC,UI_ANALYTICS_WEBSITE_ID, and optionalUI_ANALYTICS_DOMAINS. Both script src and website ID must be set; empty by default so self-hosters are not tracked unless they opt in. Replaces the hardcoded faviconapi.com analytics script in the HTML templates. - API docs & selfhosted docs — Tools offcanvas —
/apiand/docsinclude the same Tools panel as the homepage (browser search engine setup, custom URL builder, FaviconAPI Copy bookmarklet), including/#toolsdeep-link support.
Changed
- Homepage SEO —
<title>, meta description, keywords, Open Graph, Twitter Card, and JSON-LD aligned with visible body copy (full provider list, app-icon catalogs, custom URL builder). Title wording updated to “highest resolution favicon”. - Docs SEO — per-page meta description (first markdown paragraph), keywords, Open Graph, Twitter Card, and
TechArticle+BreadcrumbListJSON-LD. All doc slugs listed in/sitemap.xml(<priority>0.7</priority>)./robots.txtallows/docswhen enabled. Homepage JSON-LDsameAsincludes the docs URL. - README — expanded into a full getting-started guide (why FaviconAPI, how it works, routes, Docker quick start, configuration tables, API v1, performance tuning) mirroring the selfhosted docs content.
- Web UI navigation — Docs link added to the homepage and API docs top nav (hidden when
UI_ENABLE_DOCS=false). - Docker image —
src/docs-content/included in the build (.dockerignoreexception for markdown under that path). .env.example— documentsUI_ENABLE_DOCSand optional Umami analytics vars (UI_ANALYTICS_SCRIPT_SRC,UI_ANALYTICS_WEBSITE_ID,UI_ANALYTICS_DOMAINS).- Dependencies —
markedfor server-side markdown rendering.
[2.7.6] — 2026-07-04
Fixed
- Web UI — sized provider cards no longer vanish on failed size switch — switching Google, FaviconKit, or Favicon.run to a size that fails (upstream 502/500) no longer hides the whole card; the previous icon stays visible with a “size not available” notice instead.
- Web UI — Favicon.run size buttons match upstream — each offered size is probed on load so buttons for sizes the provider cannot serve (e.g. 16, 32, and 256 for
hosthatch.com) are hidden automatically. - Web UI — Google size buttons and routing — size availability is probed per size using the correct Google v1/v2 proxy paths (
/google/for ≤128,/googlev2/for 180/256), so working sizes like 180 stay visible when v2 succeeds even if 256 fails. - Web UI — small non-square favicons stay visible — provider preview frames use a fixed 140px-tall box with icons scaled up to 128×128 via
object-fit: contain, so tiny wide icons (e.g. Google 11×16 or 22×32 forhosthatch.com) no longer look blank or collapse the layout when switching between 32 and 16.
[2.7.5] — 2026-07-04
Added
- Web UI — placeholder results grid — on first load (and after reset), the homepage shows a skeleton preview of the provider cards so visitors see the layout before searching. Blurred icon shapes, muted size buttons, and skeleton metadata mimic real results; card interactions are disabled. Download all stays disabled until a search runs; Build custom URL remains available.
- Web UI — CTA attention on Tools and Build custom URL — subtle wiggle, glow pulse, and pointing-hand hint on the top Tools button and Build custom URL toolbar button. Animations stop on hover or after the user opens either panel (remembered for the session via
sessionStorage); respectsprefers-reduced-motion.
Changed
- Web UI — placeholder app icons follow checkbox — Also include app icon lookups now toggles the App Icons divider and service-icon cards in placeholder mode, matching behaviour after a domain search.
- Web UI — optional providers in placeholder — Brandfetch.io and logo.dev skeleton cards appear only when
BRANDFETCH_CLIENT_IDorLOGODEV_TOKENis configured (viaGET /providers), same as live results. - Web UI — HTML Scraper title link — the HTML Scraper card title links to the maflplus-favicon-api repository.
[2.7.4] — 2026-07-02
Changed
- SVGL — SVG as default — SVGL upstream assets are native SVG; the Web UI now defaults to SVG (
/svgl/0/svg/{service}) instead of rasterizing to PNG. A PNG | SVG toggle is added to the SVGL card (size buttons hide when SVG is selected), matching selfh.st, dashboardicons.com, and LobeHub. JSON discovery (/{service}/json) lists SVG as the primaryproxyfor SVGL, with a top-levelpngentry for raster URLs.
[2.7.3] — 2026-06-30
Changed
- Styled tooltips site-wide — all native browser tooltips are replaced by a Bootstrap-style CSS tooltip (
data-tip), giving a consistent dark, rounded look with a pointer arrow and fade-in. Long URLs wrap inside the tooltip, and an optional top placement (data-tip-pos="top") keeps the bookmarklet tip from being clipped in the scrollable Tools panel. Accessible names are preserved viaaria-label.
Removed
- Download-icon tooltip — the per-card Download icon button no longer shows a tooltip (the
aria-labelis kept for screen readers).
[2.7.2] — 2026-06-30
Changed
- Custom URL builder — generated URL layout — the generated URL box now stacks vertically: the URL spans the full width and wraps cleanly (smaller font), with the Copy URL button beneath it, right-aligned. Prevents the long URL and button from crowding onto one line. Scoped to the custom-URL box; the search-engine modal is unchanged.
[2.7.1] — 2026-06-30
Changed
- Custom URL builder — clearer fallback order — the four fallback selectors now show numbered badges (1-4) so the apply order is unambiguous (preferred → 1 → 2 → 3 → 4; empty slots are skipped).
Fixed
- Custom URL builder — crisp preview — the icon preview no longer forces every image to 48×48 (which upscaled and blurred small icons). It now renders at the icon's natural size inside a fixed thumbnail box, so small sizes (e.g. 16px) stay sharp.
[2.7.0] — 2026-06-30
Added
- Custom profile URLs — users can build a shareable URL
/{id}/{domain-or-appname}where{id}is a base64url string that encodes a preferred provider, up to four ordered fallbacks, and a minimum icon size (no database). On request the chain is tried in order: an SVG source satisfies any minimum and is served as-is; a raster source must be ≥ the minimum size and is then served as PNG at exactly that size; otherwise the next fallback is tried (transparent404when the chain fails). Minimum sizes:16, 32, 64, 128. Generate one via Tools → Build custom URL on the homepage (live URL, preview, and copy). New modulessrc/customProfile.js(encode/decode + validation) andsrc/profileResolve.js(chain resolver).
[2.6.3] — 2026-06-29
Fixed
- Web UI — light/dark preview contrast — light icon variants now preview on a dark background and dark variants on a light background (Brandfetch, selfh.st, dashboardicons.com, LobeHub, SVGL).
[2.6.2] — 2026-06-29
Fixed
- Catalog SVG proxy URLs — size 0 in path — SVG routes for selfh.st, dashboardicons.com, LobeHub, and SVGL now use
/0/svg/in generated proxy URLs (e.g./selfhst/0/svg/githubinstead of/selfhst/256/svg/github). Route handlers accept0as the canonical SVG path size; legacy URLs with a pixel size remain valid. JSON discovery and the Web UI emit the0segment.
[2.6.1] — 2026-06-29
Added
- Web UI — icon downloads — each favicon card has a download button in the header row (aligned with source/refresh). Download all above the results grid fetches every loaded icon and delivers a ZIP (
{query}-favicons.zip); when only one icon is available, a single file is downloaded instead. JSZip is loaded on demand for multi-file archives. Filenames use{domain-or-app}-{provider}.{ext}.
Changed
- Web UI — provider card titles link to upstream sites — each provider/app-icon card title (Google, DuckDuckGo, Yandex, Favicon.so, Vemetric, Faviconextractor, Faviconkit, Favicon.run, Brandfetch, logo.dev, selfh.st, dashboardicons.com, LobeHub, SVGL) opens the provider's website in a new tab.
[2.6.0] — 2026-06-29
Changed
- Proxy URL scheme — format in path — domain providers now use
/{provider}/{size}/{ext}/{domain}(e.g./google/128/png/github.com,/duckduckgo/32/png/github.com). Catalog providers (selfh.st, dashboardicons, LobeHub, SVGL) use/{provider}/{size}/{format}/{service}withpngorsvginstead of?format=. SVG routes use size 0 in the path (e.g./selfhst/0/svg/github,/brandfetch/0/svg/github.com); PNG routes keep pixel sizes (e.g./svgl/256/png/github,/brandfetch/128/png/github.com). Vemetric acceptspng,jpg, orwebpin the path (?format=still works). Brandfetch acceptssvg,png,webp, orjpgin the path;?type=and?theme=stay as query params. Legacy three-segment routes (/{provider}/{size}/{domain}) remain aliases; logo.dev is unchanged. /{domain}/jsondiscovery —proxyand per-size entries now include the/png/path segment for all providers that use the new scheme.- Web UI — PNG | SVG on catalog cards — selfh.st, dashboardicons.com, and LobeHub cards gain a PNG | SVG toggle; size buttons hide when SVG is selected.
- LobeHub light/dark variants — availability is probed against
@lobehub/icons-static-pngtheme assets (/light/{slug}.png,/dark/{slug}.png) instead of the color SVG URL, so Light/Dark buttons only appear when those PNGs exist. Light/dark requests serve the upstream theme PNGs; SVG format remains color-only. - Web UI — service icon controls — extension (PNG/SVG or SVG badge) and color (Color/Light/Dark) toggles on selfh.st, dashboardicons.com, LobeHub, SVGL, and Brandfetch cards are compact Bootstrap-style button groups on one row. Size buttons (256, 128, 64, …) remain separate rounded buttons with spacing.
Fixed
- ICO upstreams served on
/png/routes (e.g. DuckDuckGo's…/github.com.icoon/duckduckgo/32/png/github.com) — upstreamimage/x-iconbytes are now decoded to PNG vianormalizeEntryForPng()before resize and on legacy sizeless routes (/d/{domain},/y/{domain}, etc.). BMP ICO frames are converted BGRA→RGBA before rasterization. Failed ICO conversion no longer falls back to serving raw ICO with a PNG content-type.
[2.5.15] — 2026-06-28
Changed
- Web UI — no source/proxy toggle on tokenized providers — removed the alternate URL link from the Brandfetch.io and logo.dev cards (upstream URLs embed API tokens).
[2.5.14] — 2026-06-28
Fixed
- Brandfetch — detect actual response format — when Brandfetch serves WebP/PNG at a
.svgURL (e.g.ah.nl), the proxy now rejects the mismatched attempt and falls through to the correct raster format.X-Brandfetch-Formatand the Web UI reflect the real type, so size buttons appear for WebP/PNG.
[2.5.13] — 2026-06-28
Changed
- Brandfetch Web UI — hide dimensions for SVG — metadata no longer shows
WxHwhen the resolved format is SVG (only for PNG/WebP/JPG raster fallbacks).
[2.5.12] — 2026-06-28
Changed
- Brandfetch — SVG vs raster UI — SVG responses show a single SVG badge (no size buttons); the proxy URL uses a fixed path size and omits meaningless
h/wupstream. PNG/WebP/JPG fallbacks show size buttons (16–512) with real pixel dimensions in metadata. Upstream SVG URLs no longer includeh/wsegments.
[2.5.11] — 2026-06-28
Fixed
- Brandfetch Web UI — fixed preview frame and theme buttons — preview area stays 140×128px regardless of selected API size (size buttons only change the proxy URL/metadata). Color/Light/Dark row is hidden unless Light or Dark variants exist (probed with
?strict=1); explicit theme requests no longer fall back to the color variant, so theme switches actually change the logo.
[2.5.10] — 2026-06-28
Changed
- Brandfetch — format fallback — when
formatis omitted orsvg, the proxy tries svg → png → webp for each type/theme variant before returning 502. An explicit?format=png(orwebp,jpg) skips format fallback. Response headerX-Brandfetch-Formatindicates which format was served.
[2.5.9] — 2026-06-28
Fixed
- Brandfetch — coverage and size buttons — upstream URLs always include
h/{size}/w/{size}(including SVG). Whensymbolor a theme variant is missing, the proxy falls back throughiconandlogo(and color variants) before returning 502. Size buttons now scale the displayed icon via CSS and update the proxy URL without reloading the same SVG.
[2.5.8] — 2026-06-28
Changed
- Brandfetch — transparent SVG symbols — default upstream is now
symbol.svgwith a transparent background instead of a rastericon. Optional query params on/brandfetch/{size}/{domain}:type=icon|symbol|logo,format=svg|png|webp|jpg,theme=light|dark. Web UI defaults to Dark theme with Color/Light/Dark toggles; size buttons control display only for SVG.
[2.5.7] — 2026-06-28
Added
- Web UI alternate URL link — every favicon card except HTML Scraper now has a dashed-border link in the top-right corner that copies the opposite URL from
UI_CARD_URL: when cards show proxy URLs the link is labelled source and copies the upstream URL; when cards show source URLs the link is labelled proxy and copies the local proxy URL. Hidden for logo.dev and Brandfetch (no safe upstream URL). The homepage waits forGET /providersbefore fetching so the label matches the server'surlMode.
[2.5.6] — 2026-06-28
Added
UI_CARD_URLenvironment variable (defaultproxy) — controls which URL every favicon card shows and copies (metadata link, copy button, and click-on-icon).proxy= local proxy URL (https://your-host/google/128/example.com);source= upstream provider URL. Exposed to the Web UI viaurlModeonGET /providers. logo.dev and Brandfetch always stay on proxy (upstream URLs embed API tokens). Documented in.env.example.
Fixed
- Web UI proxy URL size did not match displayed dimensions — providers that never upscale (e.g. Yandex's fixed 16×16) still showed
/yandex/64/{domain}while the card read16×16. The displayed and copied proxy URL now uses the smallest valid path size that matches the native image (e.g./yandex/16/{domain}).
[2.5.5] — 2026-06-28
Fixed
- SVGL Web UI showed proxy URL instead of source URL — the svgl.app card metadata link and copy button pointed at the local proxy (
/svgl/{size}/{service}) rather than the upstream SVGL asset on jsDelivr. The displayed URL is now resolved from/{service}/json(endpoints.svgl.source/ per-variantsource), consistent with selfh.st, dashboardicons.com, and LobeHub.
[2.5.4] — 2026-06-28
Changed
- Brandfetch —
fallback/404and no placeholder icons — upstream URL now uses Brandfetch's documented…/h/{size}/w/{size}/fallback/404/icon?c=…form so unknown domains return 404 instead of the Brandfetch "B" mark or other generated fallbacks. Responses that are notimage/*, fully transparent, or the known 128px placeholder hash are rejected; the proxy returns 502 and the Web UI hides the card. Upstream requests send a domainRefererheader. Valid sizes: 16, 32, 64, 128, 256, 512.
Fixed
- Brandfetch Web UI shown on service lookups — searching by app name (e.g.
google) no longer leaves the Brandfetch card visible alongside selfh.st / dashboardicons / LobeHub; the card is domain-only, reset on service searches, and placed with the other favicon providers above the "App Icons" divider.
[2.5.3] — 2026-06-28
Changed
- Brandfetch — native upstream sizing —
/brandfetch/{size}/{domain}now requests the icon at the requested size directly from Brandfetch (https://cdn.brandfetch.io/{domain}/h/{size}/w/{size}/icon.png?c=…) instead of fetching a default icon and resizing server-side. Valid sizes: 32, 64, 128, 256, 512. The Web UI card gains a size-button strip;/{domain}/jsonadvertises the updated size set. Legacy/bf/{domain}defaults to 128px.
[2.5.2] — 2026-06-28
Changed
- Web UI — four provider cards per row — the results grid now uses four columns (container widened to 1280px) so more favicon cards fit on one screen without changing individual card width. The header block (logo, title, tip, search bar, API endpoint link, and footer) stays centered at three columns wide (75% / max 960px). Responsive breakpoints: two columns below 1050px, one column below 700px.
[2.5.1] — 2026-06-28
Fixed
- SVGL Web UI size buttons always linked to 128px — selecting 64 or 256 on the svgl.app card still showed and copied
/svgl/128/{service}in the metadata link becausesvglSourceUrlhardcoded size 128. The displayed URL and copy button now follow the active size button (/svgl/64/...,/svgl/256/..., etc.).
[2.5.0] — 2026-06-28
Added
- Brandfetch provider — new domain favicon provider backed by the Brandfetch Logo API. Disabled until
BRANDFETCH_CLIENT_ID(a free client ID from the Brandfetch developer portal) is set; without it the route returns 503. Source URL:https://cdn.brandfetch.io/{domain}?c={client-id}, resized server-side to 16, 32, 64, 128, 256. Canonical route:/brandfetch/{size}/{domain}, short alias:/bf/{size}/{domain}(plus the sizeless legacy/bf/{domain}). Advertised through/providers(brandfetch: true|false), listed in/{domain}/jsondiscovery, added toDEFAULT_PROVIDERoptions, and shown in the Web UI (the card and its proxy URL only appear when configured, so the client ID never leaks). Like logo.dev, Brandfetch is intentionally excluded from the best-pick race (GET /:domain) because its lettermark fallbacks would always return a generated placeholder and beat the slower scraper that finds the site's real favicon. - SVGL provider — new service-icon catalog backed by the SVGL SVG logo library (svgl.app). The catalog (~660 entries) is loaded from jsDelivr (
pheralb/svglsrc/data/svgs.ts, 24 h TTL) and SVG assets are fetched fromcdn.jsdelivr.net/gh/pheralb/svgl/static/...(the publicapi.svgl.appendpoints were not used as the primary source). Canonical route:/svgl/{size}/{service}[?variant=color|light|dark], short alias:/sv/{size}/{service}(plus the legacy/sv/{service}[?size=][&variant=]). Native sizes 64, 128, 256; SVG sources are rasterized server-side to a crisp size×size PNG. Included in/services/resolve/:service(providers.svgl),/{service}/jsonand/{domain}/json(endpoints.svgl), the best-pick race for service names and domain-derived slugs, the v1 API scraper source priority (tier afterlobehub),DEFAULT_PROVIDER=svgl, and the Web UI (new svgl.app card alongside selfh.st, dashboardicons.com, and LobeHub, with color/light/dark variant buttons, size buttons, and an "Alternative matches" panel). Theme-aware SVGL entries (separate light/dark SVG routes) expose light/dark variants when available.
Fixed
- SVGL "Alternative matches" listed unrelated icons for
github(e.g. dotenv, Esbuild, Home Assistant, Jasmine) — many SVGL catalog entries link togithub.com/...repos or*.github.iopages; the matcher treated the extracted hostname keygithubas a strong match (score 95) for any querygithub, so unrelated projects hosted on GitHub appeared as alternatives. Hostname-based scoring was removed; alternatives now match on slug/title (exact, prefix/suffix such asgithub-copilot, and existing fuzzy slug rules) only.
[2.4.0] — 2026-06-26
Added
- Google v2 — 180px size — the
faviconV2upstream also serves a 180px raster (the commonapple-touch-iconsize) that was previously usable upstream but not exposed./googlev2/{size}/{domain}(alias/g2/) now accepts180alongside16, 32, 64, 128, 256, the/{domain}/jsonlisting advertises/googlev2/180/{domain}, and the Web UI's Google card gains a180size button (routed through faviconV2, like 256). The Vemetric handler — which previously shared Google v2's valid-size set but does not serve 180 — now uses its ownVALID_VEMETRIC_SIZESset (16, 32, 64, 128, 256).
[2.3.3] — 2026-06-26
Fixed
- Catalog logo served instead of a site's own high-res icon (e.g. the sizeless
/scraper/github.comreturned the selfh.st github catalog logo even though github.com exposes its own 512×512app-icon, so the Web UI's default proxy button —/scraper/{domain}whenSCRAPER_MAX_ICON_SIZEis set — showed the catalog icon rather than GitHub's own favicon downscaled) —fetchScraperranfetchScraperCatalogFallbackbefore the direct HTML scrape whenever the domain mapped to a known service slug, overriding a perfectly good on-site icon. The direct scrape now runs first, and the curated catalogs (selfh.st, dashboardicons) are only preferred when the site's own best icon is belowMIN_SOURCE_SIZE(128px) — preserving the catalog upgrade for low-quality cases likefacebook.com(60×60 favicon) while keeping the real icon for sites that publish a large one.probeScraperCandidatesnow exposes the chosen source'ssourceWidthsofetchScrapercan make this decision; SVG sources (effective ≥512px) always count as large enough. The sized routes (/scraper/{size}/{domain}) were already unaffected because they resolve throughserveSizedScraperIconagainst the discoverediconslist. After deploying, refresh cached entries (?refresh=1) so previously cached catalog icons are replaced;X-Favicon-Sourcereturns toscraperinstead ofscraper-fallback:selfhst.
[2.3.2] — 2026-06-26
Fixed
- Wrong fallback icon for unknown brand domains (e.g.
maflplus.euserved the unrelatedmailplusenvelope icon instead of the site's own logo) — slugs derived automatically from a domain label were resolved against the selfh.st / dashboard-icons / LobeHub catalogs using Levenshtein fuzzy matching, so a brand name one character away from a catalog entry (maflplusvsmailplus, similarity 0.875 > 0.8 threshold) hijacked the result before the site's real favicon was ever scraped. Domain-derived catalog lookups are now strict (exact slug or curated/static alias only) across the scraper catalog fallback (fetchScraperCatalogFallback), the best-pick race (buildFallbackFetchers), the HTML-scraper service buckets (resolveServiceSlugForProviderSync), and the/{domain}/jsonprovider list (resolveServiceMatches(slug, { strict })). Fuzzy matching is unchanged for service names a user actually types (/{service},/sh/,/di/,/lb/). Newstrictoption added togetSelfhstSlugCandidates/getDashboardIconsSlugCandidates/getLobehubSlugCandidatesand threaded throughfetchSelfhst/fetchDashboardIcons/fetchLobehub. - selfh.st exact slug match ranked below fuzzy partials (e.g. searching
plex/plex.tvshowed Guardian (Plex) as the primary selfh.st icon instead of plain Plex) —searchSelfhstMatchesgave an exact catalog-slug match a fixed score of100, but selfh.st scoring is additive, so partial matches accumulated higher totals (guardian-plexandspotify-to-plexscored133via the-plexsuffix + name-contains bonuses) and were sorted above the exact match. Exact slug matches now receive a dominant score so they always rank first; fuzzy candidates remain as ordered "alternative matches". (Dashboard-icons / LobeHub were unaffected — their scoring usesMath.max, not addition.) - Sized scraper route upscaled tiny icons instead of using the fallback (e.g.
facebook.comonly exposes a 60×60favicon.icoto scrapers, so/scraper/128/facebook.comserved a blurry 60→128 upscale while the auto proxy/scraper/facebook.comcorrectly returned the high-res selfh.st icon) —serveSizedScraperIconnow returnsnullwhen no discovered icon can natively satisfy the requested size, letting the handler fall through tofetchScraper()(catalog / Google fallback) and resize that instead. Sizes the scraped icon can satisfy (≤ its native resolution) still use the scraped source; only sizes that would require upscaling defer to the fallback. No effect on domains whose scraped icons are already large enough.
[2.3.1] — 2026-06-26
Added
- favicon.run provider — new domain favicon provider backed by favicon.run. Upstream supports native sizes (16, 32, 64, 128, 256) via
?sz=parameter. Canonical route:/faviconrun/{size}/{domain}, short alias:/fr/{size}/{domain}. Included in/{domain}/jsondiscovery, the best-pick race (GET /:domain),DEFAULT_PROVIDERoptions, and the Web UI with size-button strip.
Fixed
- Empty Favicon.so & Faviconextractor.com cards — these providers often return SVG favicons that use CSS custom properties (
var(--primary-fill),var(--secondary-fill)) withprefers-color-schemerules. Sharp cannot resolve those variables, so rasterization produced a fully transparent PNG and the Web UI showed a blank frame while metadata still listed a size/URL. SVGs are now preprocessed with light-mode colour defaults before rasterization, and the resize proxy handlers userenderIconToSize(proper SVG/ICO handling) instead of rawdownscaleEntryToSize. - Yandex empty/transparent placeholder icons — Yandex can return a 16×16 fully transparent PNG (not just the known 1×1 case).
fetchYandexandfetchWithCachenow reject any raster icon with no visible pixels (isBlankFavicon), evict stale blank cache entries, and return 502 so the Web UI hides the card. - ICO sources on resize provider routes — when an upstream provider returns ICO bytes (e.g. Favicon.so content negotiation), resize handlers now convert to PNG via
toDisplayPngbefore downscaling so Sharp never receives an undecodable buffer.
Changed
- Upstream favicon fetch headers — provider fetches now use the same browser-like
User-Agentand raster-preferringAcceptheader as the HTML scraper instead ofFaviconProxy/1.0, reducing cases where CDNs return SVG-only responses to bot-like clients. - favicondev upstream — Favicon Extractor — the
/favicondev/provider (alias/p/) now proxieshttps://www.faviconextractor.com/favicon/{domain}instead ofhttps://favicon-3j1.pages.dev/favicon/{domain}. README,.env.example, and the Web UI direct-link were updated accordingly.
[2.3.0] — 2026-06-26
Changed
- Uniform proxy-URL scheme
/{provider}/{size}/{domain}— every provider route now follows the same consistent shape with a leading full provider name and a size path segment:/google/{size}/{domain},/googlev2/{size}/{domain},/duckduckgo/{size}/{domain},/yandex/{size}/{domain},/faviconso/{size}/{domain},/vemetric/{size}/{domain},/favicondev/{size}/{domain},/faviconkit/{size}/{domain},/logodev/{size}/{domain},/scraper/{size}/{domain}, and the catalog routes/selfhst/{size}/{service},/dashboardicons/{size}/{service},/lobehub/{size}/{service}. Previously size was sometimes a path segment (/g/{size}/), sometimes a query (?size=), and sometimes absent. /{domain}/json— uniform endpoint shape — every provider entry now exposes a consistent{ proxy, source, sizes }structure withproxypointing at the default size andsizeslisting all offered sizes using the new scheme. Provider-specific extras (vemetric.formats,scraper.icons[]/sizes/maxIconSize/fallback/wwwFallback, catalogservice/query/variants,logodev.configured) are preserved.- Web UI proxy/embed URLs — the homepage now builds and displays the new canonical URLs (e.g.
/scraper/{domain},/google/{size}/{domain},/selfhst/{size}/{service}).
Added
- Server-side resizing for sizeless providers —
duckduckgo,yandex,faviconso,favicondev,logodev(and theselfhst/dashboardiconscatalogs) now accept the size path segment and downscale the upstream icon server-side (never upscale), so the uniform/{provider}/{size}/{...}scheme is meaningful for every provider. - Backward-compatible aliases — the original short routes (
/g/,/g2/,/d/,/y/,/f/,/v/,/p/,/k/,/l/,/s/,/sh/,/di/,/lb/) keep working unchanged, including their legacy sizeless forms. - Web UI — native size detection for Google & FaviconKit cards — these providers don't actually have an icon at every offered size; they just return the largest one they have (e.g. github.com tops out at 32×32). The card now probes the proxy at its largest size to read the real native resolution and hides size buttons that exceed it, so only sizes that genuinely exist are shown (github → 32/16, apple → 64/32/16, microsoft → 128/64/32/16, notion → all). If the active size becomes unavailable, the card automatically switches to the largest available size.
Fixed
- Scraper no longer prefers catalog icons over its own —
fetchScrapernow runs the direct HTML scrape first and returns the largest discovered icon (downscaled toSCRAPER_MAX_ICON_SIZEwhen set). The curated catalogs (selfhst, dashboardicons) and Google faviconV2 are used only as a true fallback when scraping finds nothing. Previously, when a domain mapped to a known service slug, the catalog icon was returned before the site was scraped at all. - Blurry resized icons for small-source providers (e.g. Yandex's fixed 16×16) — the resize providers no longer upscale: when the upstream icon is smaller than the requested size, the native bytes are served unchanged instead of a blurry enlarged version.
- Blurry Vemetric icons — Vemetric always returns a small intrinsic SVG (~24-32px), which previously rendered tiny/soft in the UI. The SVG is now rasterized server-side to the requested size (crisp size×size PNG) so it fills the card like the other providers. Explicit
?format=png|jpg|webprequests are still served unchanged. SCRAPER_MAX_ICON_SIZEnow enforced at serve time —GET /scraper/{domain}previously only capped the icon when it was first fetched and cached. A cache entry written before the cap was configured (e.g. a full 512×512 source like github.com'sapp-icon-512) kept being served at full resolution until its TTL expired, which the browser then squeezed into the display card and rendered soft. The proxy now re-applies the cap on every response (capScraperProxyOutput), so oversized cached icons are downscaled toSCRAPER_MAX_ICON_SIZEbefore sending — no cache refresh required. The cap is a no-op when the icon is already within the limit or whenSCRAPER_MAX_ICON_SIZE=0.
[2.2.0] — 2026-06-26
Added
- SVG rasterization for scraped icons — when the HTML scraper finds an SVG favicon (e.g. hosthatch.com), it is now rasterized to PNG instead of being served as raw SVG. Previously raw SVGs were passed through to the browser, causing them to render at an arbitrary (screen-filling) size with no fixed dimensions.
/s/{domain}?size=N— scraper size parameter — the scraper proxy endpoint now accepts a?size=query parameter with standard icon sizes (16, 32, 64, 128, 256, 512). Each size is rendered directly from the original SVG source (when available) for maximum sharpness, or downscaled from the cached raster base for non-SVG icons./{domain}/json— scraper sizes in JSON — the JSON endpoint now advertisesendpoints.scraper.sizeswith proxy URLs for all six standard sizes (e.g./s/example.com?size=128).- Scraper icon proxy URLs per discovered size —
/{domain}/jsonnow includes aproxyfield on every icon inendpoints.scraper.icons[]andendpoints.scraper.wwwFallback.icons[], using the format/s/{domain}?size={width}(e.g./s/github.com?size=512). This gives every discovered icon resolution a clean, embeddable proxy URL. /{domain}/json— scraper fallback metadata —endpoints.scraper.fallbackmirrors theSCRAPER_FALLBACKsetting;endpoints.scraper.fallbackProviderreports which alternative source served the cached/s/{domain}icon (selfhst,dashboardicons,googlev2, ornullwhen the icon came from direct HTML scraping).- Web UI — scraper proxy size buttons from JSON — the HTML Scraper card now dynamically renders proxy size buttons for all sizes advertised in the JSON
sizesobject (e.g. 16, 32, 64, 128), alongside the scraped icon sizes. Sizes larger thanmaxIconSizeare omitted to prevent blurry upscaled versions. The static fallback row (512–16) is still shown when no scraped icons are found.
Changed
- SVG ranking boost in scraper — SVGs are now treated as having an effective resolution of at least 512px for candidate ranking, so they are preferred over small raster icons. Previously a 105×150 SVG viewBox would lose to a 128×128 PNG.
- SVG render density increased —
rasterizeSvgToSizenow usesdensity = size × 4(previouslysize × 1.5), producing a much larger internal canvas for crisp downscaling of vector content. - SVG per-size direct rasterization — when the scraped icon was originally an SVG, each
?size=Nrequest rasterizes directly from the preserved original SVG buffer instead of resizing from an intermediate PNG. This eliminates double-rasterization quality loss. /s/{domain}?size=N— flexible size parameter — the?sizequery parameter now accepts any integer between 1 and 1024 (previously restricted to the fixed set 16, 32, 64, 128, 256, 512). This allows the proxy to serve icons at the exact resolutions discovered by the scraper (e.g. 152, 180, 192, 384)./s/{domain}?size=N— full-resolution source selection — sized requests no longer resize from the SCRAPER_MAX_ICON_SIZE-capped cached icon. Instead,serveSizedScraperIconqueriesfetchScraperAllIcons(cached) to find the smallest source icon that is ≥ the requested size, fetches it at full resolution via the asset cache, and resizes from that crisp source. This eliminates blurry upscaling when the cached default icon was capped (e.g. requesting?size=512no longer upscales a 128px cached image).- Web UI — scraper size buttons show proxy URLs — clicking a discovered icon size button now loads and displays the proxy URL (
/s/{domain}?size=N) instead of the raw upstream source URL. The copy-to-clipboard action copies the proxy URL. Tooltips on size buttons also show the proxy URL. - Web UI — no more flashing proxy-size row — the static 512/256/128/64/32/16 proxy-size button row (
scraper-proxy-sizes) is now hidden by default and only shown when no scraped icons are found (fallback). Previously it flashed briefly before being replaced by the discovered icon sizes.
Fixed
GET /s/{domain}low-quality icons for well-known services (e.g.facebook.comwithSCRAPER_MAX_ICON_SIZE=128) — whenSCRAPER_FALLBACK=trueand the domain maps to a service slug, curated catalog icons (selfh.st SVG/PNG, dashboardicons) are now preferred before HTML scraping, then rasterized and capped to the configured max size. Previously the scraper could return a small website apple-touch-icon even when a sharper catalog icon existed.X-Favicon-Sourcereports the actual provider (e.g.scraper-fallback:selfhst).
Internal
serveSizedScraperIcon(src/index.js) — handlesGET /s/{domain}?size=Nby picking the smallest discovered source icon ≥ the requested size fromfetchScraperAllIcons, fetching it at full resolution via/s-assetcache, and downscaling withresizeIcon.- Scraper fallback helpers (
src/providers.js) —fetchScraperCatalogFallback,fetchScraperGoogleFallback,normalizeFallbackResult,getScraperFallback; catalog lookup runs before HTML scraping whenSCRAPER_FALLBACK=true, Google faviconV2 runs only after scraping fails entirely.
[2.1.0] — 2026-06-25
Added
HTML scraper provider fallback (
SCRAPER_FALLBACK, defaulttrue) — the scraper now prefers curated service-icon catalogs over direct HTML scraping for domains that map to a known service slug. Fallback chain:- Service-icon catalogs (selfh.st/icons, dashboardicons.com) — tried before HTML scraping when the domain maps to a slug via
serviceSlugFromDomain(e.g.facebook.com→facebook). Catalog icons are typically higher resolution and visually consistent. - Google faviconV2 — universal last-resort tried after HTML scraping fails completely; rejects 1×1 placeholder images.
When the domain has no slug or no catalog match, normal HTML scraping runs as before. Fallback results are processed through
toDisplayPngandcapScraperProxyOutputfor consistent PNG output, cached under the same scraper cache key, and tagged with a descriptiveX-Favicon-Sourceheader (e.g.scraper-fallback:selfhst,scraper-fallback:dashboardicons,scraper-fallback:googlev2). SetSCRAPER_FALLBACK=falseto disable and use only HTML scraping. Documented in.env.example.
- Service-icon catalogs (selfh.st/icons, dashboardicons.com) — tried before HTML scraping when the domain maps to a slug via
HTML scraper www-fallback — when scraping a bare domain (e.g.
nu.nl) finds no usable icons, automatically retries onwww.{domain}before giving up. Implemented infetchScraper(src/providers.js) andfetchBySourcePriority(src/apiScraper.js); applies transparently toGET /s/{domain}andGET /api/v1/favicon./{domain}/jsonexposesendpoints.scraper.wwwFallback({ domain, icons, proxy }) when the bare domain has an empty icon list but the www variant has icons.Web UI — HTML Scraper www-fallback notice (
index.html) — when icons are loaded from the www variant, shows a compact amber banner (No icons on {domain}: {www.domain}) with a clickable link to search the www hostname directly.UI_INCLUDE_APP_ICONSenvironment variable (defaulttrue) — controls whether the homepage checkbox “Also include app icon lookups” is checked on load. Set tofalse(or0/no/off) to leave it unchecked. Documented in.env.example; exposed to the UI viaincludeAppIconsonGET /providers.Optional scraper discovery disk cache (
SCRAPER_DISK_CACHE,SCRAPER_DISK_CACHE_DIRin.env.example)- When
SCRAPER_DISK_CACHE=true, homepage HTML, enriched icon lists (/{domain}/json), besticon JSON, parsed web manifests and icon-probe metadata are persisted as JSON under{CACHE_DIR}/scraper-discovery/(default/cache/scraper-discoveryin Docker). Entries survive container restarts and are shared across cluster workers. - TTL follows
SCRAPER_ICONS_CACHE_TTL(default 1 hour). New modulesrc/scraperDiskCache.js; wired fromsrc/providers.json read/write alongside the existing in-memory LRU caches. - Documented in
README.mdenvironment table.
- When
In-memory scraper discovery caches (
src/providers.js)fetchScraperPagenow caches homepage HTML (with in-flight deduplication so parallel/{domain}/json+/s/{domain}requests share one upstream fetch),fetchBesticonAllIconscaches besticon JSON,fetchManifestIconscaches per-manifest URL results, andprobeIconMetadatacaches per-icon probe metadata. ReusesSCRAPER_ICONS_CACHE_TTL/SCRAPER_ICONS_CACHE_MAX.
Changed
- Web UI — HTML Scraper default selection when
SCRAPER_MAX_ICON_SIZEis set (index.html) — the scraper size strip now defaults to the fast-proxy button (e.g. 128) instead of the largest discovered source icon. The/s/{domain}proxy image starts loading immediately with the results, so users no longer have to wait when selecting the capped size. WhenSCRAPER_MAX_ICON_SIZEis0(disabled), behaviour is unchanged: the largest variant is selected by default.
Fixed
- HTML scraper re-fetched discovery data on every request — only the final favicon PNG (
scraper_{domain}inCACHE_DIR) and the enriched icon list (scraperIconsCache) were cached before; HTML, manifests and probe results were fetched anew on each cache miss or after restart. Discovery layers are now cached in memory and optionally on disk. GET /s/{domain}?refresh=1only cleared the favicon disk cache — now also callsinvalidateScraperDomainCaches()to drop in-memory scraper discovery state and domain-keyed disk discovery files (page/,icons/,besticon/underscraper-discovery/).
[2.0.0] — 2026-06-25
Added
SCRAPER_MAX_ICON_SIZEenvironment variable — optional cap on the largest side (in pixels) returned byGET /s/{domain}. The scraper still selects the largest available source; when it exceeds the limit, the response is downscaled withfit: containand re-encoded as PNG before caching.0(default) serves native resolution. Does not affect/{domain}/jsonicon lists or/s-asset. Documented in.env.example;docker-compose.ymlmay ship a non-zero example (e.g.128).MANIFEST_PROBE_MAXenvironment variable (default12) — limits how many candidate web-manifest URLs the HTML scraper fetches per domain when probing for icons outside a direct<link rel="manifest">(ordered list built from HTML hints,Linkheaders, well-known paths, icon-directory heuristics andSTATIC_MANIFEST_HINTS). Stops at the first manifest that yields icons.Deep manifest discovery (
discoverManifestUrls,loadManifestIconCandidates,resolveManifestIconsinsrc/providers.js)- Parses
rel="manifest"from HTML (including combinedreltokens), HTTPLink: </manifest.json>; rel="manifest"headers, manifest URLs inferred from icon<link>directories, and well-known paths under each origin (/manifest.webmanifest,/site.webmanifest,/favicon/manifest.json, …). STATIC_MANIFEST_HINTS— direct manifest URLs for domains whose homepage is an SPA shell or bot interstitial without a discoverable manifest link (initial entry:ah.nl/www.ah.nl→ Albert Heijnsite.webmanifest).- Exported as
loadManifestIconCandidatesand consumed by both the HTML scraper (buildScraperCandidates) and the v1 API (src/apiScraper.js), so manifest-tier discovery is shared instead of duplicated with cheerio inapiScraper.js.
- Parses
endpoints.scraper.maxIconSizein/{domain}/json— reports the configuredSCRAPER_MAX_ICON_SIZEcap (or0when disabled) so the web UI can offer a fast-proxy embed button alongside full-resolution source variants.Web UI — HTML Scraper fast-proxy button (
index.html) — whenmaxIconSize > 0, a dashed lavender size button (.size-btn-proxy) loads/s/{domain}; source sizes use/s-asset. Custom tooltip with Framework7 hare icon (f7:hare-fillvia Iconify) explains the downscale-for-speed trade-off.besticon sidecar settings in
.env.example—BESTICON_TZ,BESTICON_CACHE_SIZE_MB,BESTICON_HOST_ONLY_DOMAINS,BESTICON_HTTP_CLIENT_TIMEOUT,BESTICON_HTTP_MAX_AGE_DURATION,BESTICON_HTTP_USER_AGENT,BESTICON_PORT,BESTICON_SERVER_MODE(andBESTICON_ADDRESS) with aBESTICON_prefix so they do not collide withPORT/CACHE_SIZE_MBonmaflplus-favicon-api.docker-compose.ymlmaps them via${BESTICON_…:-default}; compose reads.envfor substitution without passing the whole file into the besticon container.Documentation refresh
README.mdstreamlined: provider tables with upstream links, service-icon catalogs (incl. LobeHub/lb/), minimaldocker-compose.ymlusingenv_file: .env, environment settings delegated to.env.example, API overview, scraper?refresh=1, API v1 auth/errors, and Docker CLI key-management examples.docs/docker.md— host cache volume ownership/permissions (chown 100:101,chmod 755), and howBESTICON_URLrelates to the optional besticon sidecar.
App-name lookup on the best-pick route (
GET /{app-name})- Paths without a dot (e.g.
/jellyfin,/firefox,/immich) are treated as service names, not domains. Races selfh.st, dashboardicons and lobehub in parallel via newpickBestService()insrc/bestPick.js(same head-start /DEFAULT_PROVIDERbehaviour as/{domain}). GET /{app-name}/json— discovery JSON for service names (same catalog shape as domain JSON:best,resolve,selfhst,dashboardicons,lobehub). Example:/jellyfin/json.- Domain lookups are unchanged:
GET /github.comstill races website favicon providers.
- Paths without a dot (e.g.
Domain → icon-tag table (
src/domainIconTags.js)- Editable list of explicit
{ domain, iconTag }rows (e.g.drive.google.com→google-drive) used when v1 API scraper fallbacks and best-pick service-icon providers need a catalog slug. Lookup runs before automatic rules inserviceSlugFromDomain.js; extend the array to override or pin mappings without code changes elsewhere. GET /domain-icon-tagsreturns{ entries: [{ domain, iconTag }, ...] }for programmatic inspection. Not linked from the web UI.
- Editable list of explicit
Shared domain slug derivation (
src/serviceSlugFromDomain.js)- Centralises
serviceSlugFromDomain()consumed bysrc/apiScraper.js,src/bestPick.js,/{domain}/jsonand/services/resolve/:service. - Apex domains (
github.com) → first label (github). - Suite subdomains (
drive.google.com,teams.microsoft.com, …) →{parent}-{subdomain}when the parent label is in a built-in allow-list (google,microsoft,amazon,apple,office,live,azure). - Other subdomains (
www.github.com) →null(no service-icon lookup), avoiding ambiguous single-word slugs such asdrive→ dashboardiconseu-drive.
- Centralises
?refresh=1on/api/v1/favicon— skips the 7-day disk cache and regenerates the PNG (also acceptstrue/yes). Useful after slug-mapping or source-priority changes.Service slug alias resolution (
src/serviceAliases.js,/services/resolve/:service)- New
GET /services/resolve/:serviceendpoint resolves a search term to canonical icon slugs per catalog. Response shape:{ input, resolved, candidates, providers: { selfhst: { resolved, candidates[] }, dashboardicons: { resolved, candidates[] }, lobehub: { resolved, candidates[] } } }where each candidate includesslug,labeland a relevancescore. - Provider-specific catalogs — selfh.st, dashboardicons.com and lobehub.com use different slug names for the same product (e.g.
kdrive→ selfh.stksuite-kdrive, dashboardiconsinfomaniak-kdrive;onedrive→microsoft-onedrive; a vague term likedrivemay resolve togoogle-driveon selfh.st,eu-driveon dashboardicons anddriveon lobehub). Resolution is no longer shared across providers. - selfh.st index lookup — loads and caches selfhst/icons
index.json(24 h TTL) and matches onReference, displayNameandTags. - dashboardicons metadata lookup — loads and caches homarr-labs/dashboard-icons
metadata.json(24 h TTL) and matches on slug keys,aliasesand trailing slug segments (e.g.onedrive→microsoft-onedrive). - lobehub index lookup — loads and caches
@lobehub/iconstoc.json(24 h TTL) and matches on slug keys,title/fullTitle,id,docsUrland trailing slug segments. - Static fallbacks when metadata has not loaded yet:
onedrive→microsoft-onedrive;kdrive→ksuite-kdrive(selfh.st) /infomaniak-kdrive(dashboardicons). /sh/:service,/di/:serviceand/lb/:servicenow iterate provider-specific slug candidates and, whenvariant=coloris requested, fall back tolightthendarkif the color asset does not exist upstream.- Fuzzy slug matching (all service-icon catalogs) — slug segments and aliases that are ≥80% similar to the search term (Levenshtein ratio, min. query length 4) are treated as matches across selfh.st, dashboardicons and lobehub, so e.g.
sheetandsheetsboth resolve togoogle-sheetsvia thesheetssegment (~83% similar). Tied scores break alphabetically by slug. - Highest-score resolution — per-provider
resolvedis the top-scoring candidate, not the raw search term. The input slug is only kept as a selfh.st candidate when it exists verbatim in selfhst/iconsindex.json; otherwise resolution skips to scored matches (avoiding phantom slugs likesheetorsheetsthat do not exist as PNG filenames). - selfh.st name/tag matching — in addition to fuzzy slug segments, selfh.st scores substring hits on display
NameandTagsfrom its index (e.g.sheets→google-sheetsvia "Google Sheets"). - Hyphenated slug guard (selfh.st) — when the input slug contains a hyphen but is not in the selfh.st catalog (e.g. dashboard-only
eu-drive), only that exact slug is tried; fuzzy partial matches likedrive-synologyare not substituted. - Variant toggles follow upstream assets — Color/Light/Dark buttons on each service card are shown only after a probe confirms the variant exists upstream. Catalogs differ: dashboardicons often ships color-only PNGs (e.g.
microsoft-onedrive.pngwith no-light/-darksiblings), while selfh.st may publish separate-light/-darkfiles and list legacy slugs such asmicrosoft-onedrive-2018under Alternative matches. - Web UI — "Alternative matches" panel on all three service-icon cards: when multiple slug candidates exist for a provider, a grid of clickable tiles (thumbnail, label, slug) is shown below the main preview so users can switch between matches (e.g. pick
ksuite-kdriveon selfh.st after searchingkdrive, ormicrosoft-onedrive-2018after searchingonedrive). The search-options hint lists per-provider resolved slugs when they differ (e.g.selfh.st: "google-drive", dashboardicons: "eu-drive", lobehub: "drive"). Service-icon cards load via the resolved slug (/sh/{resolved},/di/{resolved},/lb/{resolved}); cards hide when the color variant fails to load during domain or service searches (hideOnError). /{domain}/json—endpoints.selfhst,endpoints.dashboardiconsandendpoints.lobehubnow expose the provider-specific resolved slug inservice, the raw domain label inquery, and build proxy/source URLs from the resolved slug. v1 scraper fallback tiers and the best-pick cascade use the same per-provider resolution.
- New
FaviconAPIs-compatible JSON API (
/api/v1/favicon+/cdn/favicons/{domain}.png)- New
GET /api/v1/favicon?url={website}endpoint returns a JSON object —{ url, domain, width, height, format, sourceType, cached, cachedAt }— instead of the image bytes, modelled on faviconapis.com/docs. The PNG itself is fetched separately from the returned CDN URL. - Source priority for icon discovery is
svg>manifest>apple-touch-icon>png>selfhst>dashboardicons>lobehub>external. The first source that yields a decodable image wins, and the tag is reported in thesourceTyperesponse field. Implemented in newsrc/apiScraper.js, reusing existing scraper helpers (fetchScraperPage,parseIconCandidatesFromHtml,fetchManifestIcons,fetchScraperAsset) rather than duplicating HTML/manifest fetch logic. Root/favicon.icoand other ICO sources are excluded — they contain small frames that upscaled poorly. - All output is normalized to a uniform 128×128 PNG via new
src/imageNormalize.js. SVGs are rasterized atdensity=192(4×96 dpi) for a crisp 128-pixel render; raster sources must be at least 128×128 and are downscaled withfit: containand a transparent background. Sources larger than 128px are preferred; exactly-128px sources are only used when nothing larger is available. - 7-day CDN cache. The generated PNG is written to
API_CACHE_DIR(default/cache/api) with a sibling.meta.json({ sourceType, width, height, cachedAt }), and re-served via the new publicGET /cdn/favicons/{domain}.pngroute withContent-Type: image/pngandCache-Control: public, max-age=604800, immutable. Subsequent calls within the TTL window returncached: trueand the originalcachedAttimestamp;cdn/favicons/...returns404until/api/v1/faviconhas been called at least once for that domain (safe to expose publicly). Cache entries whose PNG or metadata dimensions are not 128×128 are treated as stale and regenerated on the next request. - Error responses match the FaviconAPIs spec and are always JSON with
errorandcodefields:400 missing_url/400 invalid_url,401 missing_api_key/401 invalid_api_key,422 favicon_not_found/422 favicon_not_processable(withsourceType+sourceUrlfor diagnostics),429 quota_exceeded(withplan/limit/used/period),500 internal_error. - New environment variables:
API_KEYS_DB(default/cache/api-keys.sqlite),API_CACHE_DIR(default/cache/api),API_CACHE_TTL(default604800= 7 days, used both for cache freshness and the CDNCache-Control: max-age). - Mounted in
src/index.js(app.use(apiRoutes)) directly after the static-assets handler and before the catch-all/:domainprovider route so/api/...and/cdn/...paths take precedence. Existing routes (/g/...,/g2/...,/d/...,/y/...,/f/...,/v/...,/p/...,/k/...,/l/...,/s/...,/sh/...,/di/...,/lb/...,/{domain},/{domain}/json) are unaffected.
- New
API key authentication with per-key monthly quotas (SQLite-backed)
- New
src/apiStore.jspersists API keys and per-key monthly usage in a SQLite file atAPI_KEYS_DB(default/cache/api-keys.sqlite, inside the existing cache volume so it survives container restarts and is shared between cluster workers via WAL mode +synchronous=NORMAL+foreign_keys=ON). Only the SHA-256 hash of each raw key is stored server-side; the raw key is shown exactly once at creation time. - Keys are passed either as
Authorization: Bearer <key>or?key=<key>. Keys use the prefixfa_followed by 24 base32-style characters drawn from anabcdefghijkmnopqrstuvwxyz23456789alphabet (no0/O/1/Iconfusion), giving roughly 120 bits of entropy per key. - Three plans configurable via env vars, defaults mirror FaviconAPIs:
PLAN_FREE_LIMIT=25,PLAN_PRO_LIMIT=2500,PLAN_ENTERPRISE_LIMIT=0(where0 = unlimited). The plan assigned at key creation determines the monthly cap; that cap is snapshotted intoapi_keys.monthly_limitso changing the env var afterwards does not retroactively change existing keys. - Quotas reset per calendar month UTC (
period = YYYY-MM). A request only counts toward the quota when the API returns200, matching FaviconAPIs' "successful HTTP response" rule —4xx/5xxcalls do not consume quota.cached: trueresponses do count (the API still authenticated you and returned a valid result). - When the cap is reached, subsequent calls return
429 quota_exceededwith{ plan, limit, used, period }in the response body. - Two tables:
api_keys(id,key_prefix,key_hash,label,plan,monthly_limit,status,created_at) andusage_monthly(api_key_id,period,count, PK =(api_key_id, period), withON DELETE CASCADEso deleting a key wipes its history). Increment usesINSERT ... ON CONFLICT DO UPDATE SET count = count + 1.
- New
API_REQUIRE_KEYenvironment variable to disable auth and quotas entirely- Default
true(current behaviour: key required, quota enforced). - Set to
falseto make/api/v1/favicona fully public endpoint — noAuthorizationheader or?key=required, and per-key plans/quotas are not enforced. A provided key is silently ignored in this mode (not validated, not tracked). Useful for self-hosted deployments behind their own auth layer or for fully open instances. - Accepts
false/0/no/off(case-insensitive) as off-values; an empty or unset value falls back to the defaulttrue. - Plan-related env vars (
PLAN_FREE_LIMIT,PLAN_PRO_LIMIT,PLAN_ENTERPRISE_LIMIT) are only applied whenAPI_REQUIRE_KEY=true. - Documented in
README.mdand.env.example.
- Default
CLI for API key management (
scripts/manage-keys.js, wired as npm scripts)npm run keys:create -- --label "customer A" --plan free|pro|enterprise— prints the rawfa_<24 chars>key exactly once (only its SHA-256 hash is persisted) along with the plan and monthly limit. The displayed prefix inkeys:listis the first 11 characters (fa_+ 8 random) — long enough to revoke unambiguously without leaking the secret.npm run keys:listshows active keys with this month's usage counter;npm run keys:list -- --allalso includes revoked keys (kept for audit history).npm run keys:revoke -- --prefix fa_abcd1234flipsstatustorevokedso the key stops validating immediately, but keeps the row in the DB and excludes it from the defaultkeys:listoutput.npm run keys:delete -- --prefix fa_abcd1234permanently removes the key row and (viaON DELETE CASCADE) its usage counters.- Inside Docker, invoke via
docker compose exec maflplus-favicon-api npm run keys:create -- --label "..." --plan proso the script writes to the same SQLite file the running server reads from.
api.html- interactive API documentation and playground for the new v1 API, served at/apiand/api.html.- New user-facing page (
src/public/api.html) styled to match the homepage (same#faf6f1background,#5b2e7epurple accents, Georgia serif headings) and registered viaapp.get(['/api', '/api.html'], renderTemplate(API_HTML_TEMPLATE))insrc/index.js. - Templated rendering shared with the homepage:
src/index.jsfactored the per-requesthttps://favicon.newaisite.comsubstitution into arenderTemplate(template)helper now mounted on both['/', '/index.html']and['/api', '/api.html']. Canonical link, Open Graph, Twitter Card and JSON-LD (@type: TechArticle,isPartOf: WebApplication) tags are populated with the request's absolute origin without baking the hostname into the image.express.staticstill runs with{ index: false }so/api.htmlalways goes through the templated route. - Quick start with a copy-able
GETexample and tabbed code samples forcurl,JavaScript(browserfetch),Node.js(undicirequest),Python(requests),PHPandPowerShell(Invoke-RestMethod). Each sample is regenerated client-side fromlocation.originso it shows the exact URL the visitor is on (noyour-hostplaceholders). - Interactive playground ("Try it") that sends a real request from the browser:
- Free-text URL input plus optional API key field with an
Authorization: Bearer/?key=toggle for how the key is sent. - Live response panel showing a status pill (
200,400,401,422,429,500) in mode-appropriate colors, the request URL, elapsed time in ms, and the formatted JSON body. - Side preview frame that renders the resulting
/cdn/favicons/{domain}.pngplus dimensions andcached/sourceTypenotes when the call succeeds; "Request did not return an image" placeholder on error. - Quick-domain chips,
Copy JSONbutton,Clearbutton, andlocalStoragepersistence of the entered key across page loads.
- Free-text URL input plus optional API key field with an
- Endpoint reference with query-parameter and response-field tables; Errors table with colored status pills per code (400, 401, 422, 429, 500); Authentication section (Bearer header +
?key=examples + bundledscripts/manage-keys.jsCLI usage); Plans & quotas table populated at runtime from/providers; CDN route documentation. - Adaptive UI based on the server's
API_REQUIRE_KEYsetting - the page fetches/providerson load and:- Shows an orange "API keys required" badge in the header when keys are enforced; hides the banner entirely when this instance runs without required keys.
- Hides the playground key input, the Authentication section, the Plans & quotas section, and the Errors-section note that only
200responses count toward the monthly quota when this instance does not require a key. - Re-renders the Quick start code samples so the
Authorization: Bearer fa_your_key_hereheader (or?key=...) is dropped when not relevant - the samples become valid copy-paste calls for the actual running instance.
- All env-var names (
API_REQUIRE_KEY,API_CACHE_TTL,PLAN_*_LIMIT) are kept out of the user-facing copy; runtime/config documentation lives inREADME.mdand.env.example. /providersextended withapi.{ requireKey, cacheTtl, plans }so the docs page can adapt without a separate endpoint.requireKeymirrors theAPI_REQUIRE_KEYparsing insrc/apiRoutes.js(truthy unless the env var equalsfalse/0/no/off),cacheTtlreflectsAPI_CACHE_TTL(default 604800),plansexposesapiStore.PLAN_LIMITS(free/pro/enterprise)./robots.txtallow-list extended with/apiand/api.html;/sitemap.xmlincludes a new<url>entry for/api(<priority>0.8</priority>,<changefreq>monthly</changefreq>) so search engines can index the docs page.- Homepage
top-navgains an API link pointing at/api, sitting between the Tools offcanvas button and the Wiki external link.
- New user-facing page (
Consistent top navigation across pages —
api.htmlnow has the same top-nav asindex.html: Tools offcanvas (bookmarklet + browser search-engine setup modal), Home, API and Wiki links. A Home menu item (/) has been added to both pages, with the current page highlighted via anactivestyle.Page footer on
index.htmlandapi.html— both pages share a footer with a FaviconAPI brand label and GitHub links to the MAFL+ and FaviconAPI repositories, separated from the main content by a subtle light-gray top border.Site favicon pack (
src/public/favicons/)- Full multi-size favicon set:
favicon.ico, PNGs at 16/32/48/96 px,apple-touch-icon.png(180×180), Android Chrome icons (192/512),ms-icon-144x144.png, and amanifest.jsonfor PWA install tiles. - Wired into the
<head>of bothindex.htmlandapi.html: sizedrel="icon"links, shortcut icon, apple-touch-icon, web app manifest link, andmsapplication-TileColor(#5b2e7e) /msapplication-TileImagemeta tags.
- Full multi-size favicon set:
logo.svg— vector site logo for the page header (<img class="site-logo">).v1 API fallback sources (
src/apiScraper.js)- Standard well-known paths — probes
/apple-touch-icon.png,/apple-touch-icon-precomposed.png,/android-chrome-512x512.pngand/android-chrome-192x192.pngwhen HTML does not declare them. NxNsize-variant expansion — HTML candidates matching…/{N}x{N}.png(e.g. Reddit's64x64.png) are expanded to larger CDN variants (128–512) before probing, reusingexpandSizedVariantsfromsrc/providers.js.- selfhst, dashboardicons & lobehub — when scraping finds nothing usable, looks up the domain's first label as a service slug (e.g.
google.com→google) against selfhst/icons, homarr-labs/dashboard-icons and @lobehub/icons-static-svg via jsDelivr/unpkg. - Google faviconV2 (
sourceType: external) — last-resort fallback requesting a 256px icon fromt0.gstatic.com/faviconV2and downscaling to 128×128.
- Standard well-known paths — probes
Browser custom search engine (
/search?q=...+/opensearch.xml)- New
GET /search?q={query}route redirects to/?q={query}so the homepage loads favicon results for the typed domain or service name. Intended URL for browser search-engine settings:https://your-host/search?q=%s. - New
GET /opensearch.xmlOpenSearch descriptor (linked from the homepage<head>) for one-click "Add search engine" in Firefox, Chrome and other OpenSearch-aware browsers. - The homepage reads
?q=on load and auto-runs a lookup. - Web UI — "Search from browser" modal: step-by-step setup instructions per browser (Chrome, Edge, Firefox, Safari) with the search-engine URL shown prominently (click-to-copy and as clickable links per section). The URL is derived from
location.originat runtime. - Web UI — "Tools" offcanvas: the browser-search and bookmarklet actions moved out of the main page flow into a slide-in panel opened from a Tools button in the top navigation. Keeps the homepage uncluttered while both shortcuts remain one click away. Closes on backdrop click or Escape; opening the search modal closes the offcanvas first.
/#toolsdeep link — visiting/#toolson the homepage or API docs page (index.html,api.html) opens the Tools offcanvas automatically (also onhashchange), enabling shareable links such ashttps://faviconapi.com/#tools.
- New
homarr-labs/dashboard-icons lookup (
/di/{service})- New service-name lookup against the homarr-labs/dashboard-icons catalog via
cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/.... - Supports
?variant=color|light|darkwith the same-light/-darksuffix convention used for/sh/. Color variant uses the bare slug (/di/jellyfin→png/jellyfin.png); light/dark map topng/{slug}-light.pngandpng/{slug}-dark.png. - Service slug validation reused from
/sh/: lowercase alphanumerics with.,_,-. - Returns HTTP 404 when the upstream icon does not exist (caches the negative result through the existing on-disk cache layer).
- New
dashboardiconscache provider key +fetchDashboardIcons(service, variant)exported fromsrc/providers.js;PROVIDERS.dashboardIcons(service, variant)builds the upstream URL. /{domain}/jsonnow exposes the new source alongsideselfhstunderendpoints.dashboardicons(same shape:service,proxy,source,variants.{color,light,dark}.{proxy,source}).null-valued fields when the domain has no usable slug (i.e. no first label).- Best-pick cascade (
/{domain}):dashboardiconsis added to the candidate set when a domain has a derivable service slug, and is a valid value forDEFAULT_PROVIDER..env.example,README.mdand the Environment Variables table updated to list the new value. - Web UI: new "Dashboard Icons (homarr)" card rendered side-by-side with the selfh.st card under the same
data-card-type="service"group and the same color/light/dark variant probe pipeline (variant buttons auto-hide when the upstream variant does not exist). The existing search-options checkbox now controls all three service-icon lookups (relabelled to "Also include service icon lookups") and the meta description / Open Graph / Twitter Card / JSON-LD description / keywords in<head>mention all three catalogs for SEO.
- New service-name lookup against the homarr-labs/dashboard-icons catalog via
LobeHub icons lookup (
/lb/{service})- New service-name lookup against the @lobehub/icons-static-svg catalog via
cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@latest/icons/.... - Supports
?variant=color|light|darkand?size=64|128|256(default 128). Upstream assets are SVG; the proxy rasterizes them to PNG viarasterizeSvgToSizefromsrc/imageNormalize.js. Light/dark variants are synthesized by recoloring non-transparent pixels to white/black after rasterization (LobeHub ships a single SVG per icon, not separate light/dark files). - Icon URL priority for the color variant:
{slug}-color.svg→{slug}-brand-color.svg→{slug}.svg→{slug}-brand.svg, guided by flags in the cached@lobehub/iconstoc (hasColor,hasBrandColor,hasBrand). - Service slug validation reused from
/sh/: lowercase alphanumerics with.,_,-. - Returns HTTP 404 when no upstream SVG exists (cached as a negative result through the existing on-disk cache layer).
- New
lobehubcache provider key +fetchLobehub(service, variant, size)exported fromsrc/providers.js;PROVIDERS.lobehub(service, variant)builds the upstream URL. /{domain}/jsonnow exposes the new source underendpoints.lobehub(same shape as selfhst/dashboardicons, plus asizesarray for 64/128/256 proxy URLs).- Best-pick cascade (
/{domain}):lobehubis added to the candidate set when a domain has a derivable service slug, and is a valid value forDEFAULT_PROVIDER..env.exampleupdated to list the new value. - Web UI: new "lobehub.com" card alongside selfh.st and dashboardicons under the same
data-card-type="service"group, with color/light/dark variant buttons (light/dark auto-hide when unavailable), size buttons (64/128/256) and the same "Alternative matches" panel. The search-options hint lists per-provider resolved slugs for all three catalogs.
- New service-name lookup against the @lobehub/icons-static-svg catalog via
SEO:
/robots.txt,/sitemap.xmland a templated homepage- New
GET /robots.txtroute serves an allow-list: only the homepage and the three static assets (/favicon.png,/logo.png,/sitemap.xml) are indexable; everything else (the/g/...,/g2/...,/d/...,/y/...,/f/...,/v/...,/p/...,/k/...,/l/...,/s/...,/sh/...,/s-asset,/providers,/{domain}and/{domain}/jsonendpoints) is disallowed so search engines don't enumerate the unbounded favicon URL space. TheSitemap:directive is auto-built from${req.protocol}://${req.get('host')}. - New
GET /sitemap.xmlroute returns a single-URL sitemap pointing at the homepage, with<loc>derived from the request host (so it picks up the public origin behind any reverse proxy via the existingtrust proxysetting; no env var required). - The homepage (
/and/index.html) is now served by a smallrenderIndexroute that loadssrc/public/index.htmlonce at startup and substituteshttps://favicon.newaisite.comtokens with the request's absolute origin before sending. This populates<link rel="canonical">, Open Graph (og:url,og:image), Twitter Card (twitter:image) and JSON-LD (schema.org/WebApplication) absolute URLs automatically, without baking the hostname into the image. express.staticis now mounted with{ index: false }so requests for/and the raw/index.htmlalways go through the templated route — preventing the unrenderedhttps://favicon.newaisite.complaceholders from ever leaking to the browser. All other static assets (/favicon.png,/logo.png) continue to be served byexpress.static.
- New
SEO meta in
src/public/index.html<head>- Descriptive
<title>(MAFL+ Favicon API – Get high-resolution favicons for any domain) and metadescription,keywords,author,robots,theme-color,color-scheme,application-name. <link rel="canonical">,<link rel="apple-touch-icon">and<link rel="sitemap">for auto-discovery.- Full Open Graph card (
og:type,og:site_name,og:title,og:description,og:url,og:image+og:image:type/width/height/alt,og:locale) for Facebook / LinkedIn / Discord / Slack unfurls. - Twitter Card (
summarywithtwitter:title/description/image/image:alt). - JSON-LD structured data (
@type: WebApplication, withname,description,url,image,applicationCategory: DeveloperApplication, freeOffer,author,isPartOf: MAFL+andsameAsto the GitHub repos and the MAFL+ wiki). - Documented in
README.mdunder a new "SEO" section and the API table rows for/robots.txtand/sitemap.xml.
- Descriptive
CACHE_SIZE_MBenvironment variable caps the total size of the disk cache (CACHE_DIR) in megabytes. When the directory exceeds the configured limit, the oldest entries (bymtime) are evicted — both the data file and its.metasibling — until the cache is back under the cap. Each cluster worker keeps a lightweight in-memory index of disk files and rescans the shared cache directory every 60 seconds (and on every set that pushes its local view over the limit) so writes from sibling workers converge into a single accurate view before eviction runs. Set to0(default in code) to disable the size cap and fall back to the original TTL-only behaviour. Documented inREADME.mdand.env.example.besticon integration for the HTML scraper
- New
BESTICON_URLenvironment variable points at a sidecar besticon instance (e.g.http://besticon:8080). - When set,
/s/{domain}first asks besticon's/allicons.json?url={domain}for the icon list, then runs the candidates through the existingsharp-validated probe pipeline (fetchBesticonCandidates→rankCandidates→probeScraperCandidates). Falls back to the built-in HTML scraper (fetchScraperPage+buildScraperCandidates) when besticon is unreachable or returns no usable candidates. - The resulting entry keeps
provider: 'scraper'so theX-Favicon-Sourceheader, the scraper cache key (?refresh=1flow) and the/{domain}/jsonlisting all stay backward-compatible. /{domain}/jsonnow exposes the full discovered icon list underendpoints.scraper.icons(each entry hasurl,width,height,format,bytes); empty array whenBESTICON_URLis unset or besticon errors out.- New asset proxy
/s-asset?url=...that fetches arbitrary upstream icon URLs server-side using the scraper's existing header retry strategy (bare → minimal → Sec-Fetch chrome headers). Cached on disk + LRU keyed by SHA-1 of the URL; SSRF-guarded against localhost /127.,10.,192.168.,169.254.,::1, link-local and ULA IPv6 ranges; max URL length 2048; onlyhttp(s). Used by the UI so CDN Referer/hotlink protection on hosts likeredditstatic.comorredgifscannot break direct browser<img>loads. - HTML Scraper card in the web UI now renders a row of size-selector buttons (same
.size-btnstyling as Google/Faviconkit), populated fromendpoints.scraper.iconsand sorted descending bywidth × height. Clicking a size:- Idx 0 (largest) keeps loading via the canonical
/s/{domain}proxy so click-to-copy on the image still yields the embeddable scraper URL. - Idx > 0 loads via
/s-asset?url=..., with the meta row showing{w}×{h}and the upstream source URL plus a copy button (consistent with Google's behaviour). - Race-protected via a
currentScraperIdxguard so a slow variant load cannot overwrite a newer selection.
- Idx 0 (largest) keeps loading via the canonical
- (Superseded when
SCRAPER_MAX_ICON_SIZEis set — see Changed below: source variants load via/s-asset; a separate fast-proxy button serves/s/{domain}.) - Bundled
docker-compose.ymlships besticon as an internal-only service: noports:mapping (the besticon frontend at/is not publicly reachable), health-checked on/up, joined to a sharedbesticonbridge network somaflplus-favicon-apican resolve it on hostnamebesticon.maflplus-favicon-apideclaresdepends_on: besticon: { condition: service_healthy }. SetBESTICON_URL=http://besticon:8080in.env(see.env.example) to enable the sidecar. - Documented in
README.mdand.env.example:- API table row for
/s-asset?url=...covering the SSRF guard (localhost / private IPv4 ranges, link-local / ULA IPv6,http(s)only, max URL length 2048) and that the route is used by the UI for upstream icons whose CDN blocks direct browser<img>loads. /s/{domain}row updated to mention besticon delegation via/allicons.json?url=...and the built-in scraper fallback./{domain}/jsonexample documenting the newendpoints.scraper.iconsarray.- Docker section rewritten to show the actual two-service compose file (incl. the besticon sidecar with healthcheck, the shared
besticonbridge network anddepends_on: { condition: service_healthy }) plus a note on swappingimage:forbuild: .and how to run the stack without besticon (built-in scraper fallback). - Environment Variables table entry for
BESTICON_URL.
- API table row for
- New
HTML scraper provider (
/s/{domain})- Parses the target site's HTML for
<link rel="icon">,shortcut icon,apple-touch-icon,apple-touch-icon-precomposed, andfluid-icon. - Reads the web app manifest (
<link rel="manifest">) and merges its declared icons into the candidate list. - Honours
<base href>and follows redirects to resolve relative URLs against the final document URL. - Adds standard fallbacks:
/apple-touch-icon.png,/apple-touch-icon-precomposed.png,/android-chrome-512x512.png. - Probes well-known larger size variants on CDN paths that follow an
NxNnaming pattern (e.g.64x64.png→128x128.png,256x256.png,512x512.png) to recover hi-res icons that SPAs only inject client-side. - Score-ranks candidates by declared
sizesattribute and format (SVG > PNG > WebP > ICO), then verifies real image dimensions viasharpand picks the largest valid result. - Skips non-display icon types when ranking: Safari
mask-icon/ pinned-tab SVGs, and web-app-manifest icons withpurpose: monochrome(or matching URL patterns). - New
cheeriodependency for HTML parsing. - Scraper cache bypass:
/s/{domain}?refresh=1(or?nocache=1) clears the cached entry and re-scrapes.
- Parses the target site's HTML for
Google v2 provider (
/g2/{size}/{domain})- Uses
t0.gstatic.com/faviconV2for higher-resolution Google icons. - Supported sizes: 16, 32, 64, 128, 256.
- Uses
Faviconkit provider (
/k/{size}/{domain})- Uses
ico.faviconkit.net. Supported sizes: 16, 32, 64, 128, 256.
- Uses
logo.dev provider (
/l/{domain})- Optional, gated by the new
LOGODEV_TOKENenvironment variable. - Returns HTTP 503 when the token is not configured.
- Optional, gated by the new
selfhst icons lookup (
/sh/{service})- Service-name lookup against the selfhst icons catalog via
cdn.jsdelivr.net. - Supports
?variant=color|light|dark. - Service slug validation: lowercase alphanumerics with
.,_,-.
- Service-name lookup against the selfhst icons catalog via
Provider configuration endpoint (
/providers)- Reports which optional providers are enabled (currently
logoDev) and exposes the publishable logo.dev token to the UI for direct image references. - Reports
upstreamIpv4: truewhen upstream fetches are forced over IPv4 (datacenter/VPS compatibility). - Reports
defaultProviderwhenDEFAULT_PROVIDERis configured.
- Reports which optional providers are enabled (currently
/{domain}/jsonendpoint expansion- Now includes Google v2, Faviconkit (sized variants), Vemetric (default + sized + format variants), logo.dev, HTML scraper and selfhst entries (with
color/light/darkvariants). - Each endpoint exposes both a
proxyURL (this server) and asourceURL (upstream provider) for transparency.
- Now includes Google v2, Faviconkit (sized variants), Vemetric (default + sized + format variants), logo.dev, HTML scraper and selfhst entries (with
Best-pick cascade (
/{domain}) updated to include the new providers- New default fallback order: scraper → Google v2 → DuckDuckGo → Google → Faviconkit → Favicon.so → Vemetric → Favicon-3j1 → Yandex.
- When
LOGODEV_TOKENis set, logo.dev is inserted near the top of the cascade.
Web UI
- New cards for HTML Scraper, Faviconkit (with size buttons 16–256), logo.dev (only shown when the server reports it as configured) and selfhst icons (with color/light/dark variant buttons).
- Search input now accepts both a domain (e.g.
example.com) and a bare service name without a TLD (e.g.radarr,sonarr); when no dot is present the input is treated as a service-icon name and all three service-icon cards (selfh.st, dashboardicons.com, lobehub.com) are shown. - "Also include selfhst icon lookup when searching a domain" toggle to additionally probe a derived service slug for any domain query.
- Quick-link suggestions extended with self-hosted service examples (
firefox,immich,jellyfin). - Front-end fetches
/providerson load to conditionally show or hide the logo.dev card. - Favicon preview images append a cache-busting query parameter so browser cache does not show stale scraper results after redeploy.
- HTML Scraper card shows the proxy URL (
/s/{domain}) under the icon — the URL to use in apps — not the scraped site's homepage.
upstreamFetchmodule (src/upstreamFetch.js)- Shared IPv4-only undici
fetchwrapper with an optional HTTP/1.1 dispatcher (allowH2: false) for scraper retries against origins that reject HTTP/2 from datacenter IPs.
- Shared IPv4-only undici
Cluster mode (
src/cluster.js)- New entrypoint that forks one worker per CPU core using Node's built-in
clustermodule; the DockerCMDnow bootssrc/cluster.jsinstead ofsrc/index.js. - Crashed workers are automatically respawned with a log line.
- Configurable via the new
WORKERSenvironment variable; defaults toos.cpus().length. SetWORKERS=1to run as a single process (clustering disabled). Note: in Docker,os.cpus()reports the host's core count rather than the container's CPU limit — setWORKERSexplicitly when constraining CPU.
- New entrypoint that forks one worker per CPU core using Node's built-in
Performance / concurrency configuration
- New
UV_THREADPOOL_SIZE(default16in.env.example) raises Node's libuv thread pool above the built-in default of4, giving more headroom for parallel disk I/O (cache reads/writes) and DNS lookups under load. - New
PICK_HEAD_START_MS(default150ms) controls the head-start given to the preferred provider in the new parallel/{domain}race (see "Changed" below). - New
SCRAPER_PROBE_BATCH_SIZE(default4) controls how many HTML scraper icon candidates are probed in parallel per batch.
- New
Configuration
- New
DEFAULT_PROVIDERenvironment variable to set the preferred provider for/{domain}requests. Valid values:scraper,google,googlev2,duckduckgo,yandex,faviconso,vemetric,favicondev,faviconkit,logodev,selfhst,dashboardicons,lobehub. Other providers race in parallel after the head-start window. Logs a warning at startup when an invalid value is supplied. - New
LOGODEV_TOKENenvironment variable, documented inREADME.mdand.env.example.
- New
Documentation
README.mdrewritten to cover all new endpoints, the size matrix per provider, the selfhst lookup, the/providersendpoint, and theLOGODEV_TOKENvariable.- Endpoint table consolidated to use parameterised paths (e.g.
/g/{size}/{domain}) instead of one row per size. - Documented scraper cache bypass:
/s/{domain}?refresh=1(alias?nocache=1).
Response headers
- Favicon responses may include
X-Favicon-Urlwith the upstream asset URL when known (e.g. after HTML scraper fetch).
- Favicon responses may include
Changed
README.mdintro — replaces the one-line tagline with a fuller project summary, a What it does section, links to the live demo (faviconapi.com) and browser tools (faviconapi.com/#tools), and short descriptions of the custom search engine and FaviconAPI Copy bookmarklet; favicon-provider table heading simplified./{domain}/jsonand/{app-name}/json— reliable catalog entries- Service-icon blocks are built only when a provider actually resolves a slug (
resolveServiceMatcheson domain JSON; no more guessing the raw search term asselfhst/lobehubslug). variantslists only upstream assets that exist — selfh.st and dashboardicons light/dark URLs are included only after a CDN probe confirms the PNG (24 h in-memory cache per slug). Fixes phantom entries such as dashboardiconsreddit-light.png/reddit-dark.pngwhen only the color PNG exists.- Domain JSON now uses the same async
resolveServiceMatches()path as service-name JSON (replacing per-provider sync resolve). - JSON discovery responses use
Cache-Control: no-cacheinstead ofmax-age=86400, so variant lists and provider fixes are not stuck in the browser for a day after deploy.
- Service-icon blocks are built only when a provider actually resolves a slug (
Web UI — copy feedback and example URLs (
index.html)- Tip / example URL documents
{domain}or{app-name}; clicking the example copies via the global bottom-right toast (same as image copy), not an inline message in the green tip box. - Service-name searches show the short best-pick URL (
/{app-name}) and re-enable the View JSON link (/{app-name}/json). - Service-icon cards are omitted when
/services/resolvereturns no candidates for that provider (instead of probing a guessed slug and failing). - Search reset — a clear (
×) button beside the search field appears after a lookup;resetSearch()clears the query, hides all provider cards and restores the empty state.
- Tip / example URL documents
Web UI — HTML Scraper size strip (
index.html) — reworked whenSCRAPER_MAX_ICON_SIZEis set and/{domain}/jsonexposesendpoints.scraper.maxIconSize:- On load, selects the largest discovered source (
/s-asset) instead of auto-selecting the downscaled fast-proxy (/s/{domain}). - Adds a hare fast-proxy button (Iconify
f7:hare-fill) for the embeddable/s/{domain}URL atmaxIconSize, with a custom tooltip explaining faster loads via downscaling; source and proxy buttons are de-duplicated when they share the same declared size (source listed first). - Fast-proxy meta row always shows
maxIconSize×maxIconSize(not loaded pixel dimensions); retries once with?refresh=1when a stale/s/disk cache still serves an oversized image. - Race-safe loading via
scraperLoadToken; meta row distinguishes proxy URL vs upstream source URL for copy. - When
SCRAPER_MAX_ICON_SIZEis0, behaviour unchanged: largest variant uses/s/{domain}directly.
- On load, selects the largest discovered source (
docker-compose.ymlconfiguration via.env—maflplus-favicon-apiloads settings fromenv_file: .env.example(copy to.envand edit for local overrides) instead of a long inlineenvironment:block. Besticon receives only theBESTICON_*variables mapped in itsenvironment:section via${BESTICON_…:-default}substitution from the same.envfile. Defaults tobuild: .for local development; publishedghcr.ioimage line commented out..env.example— every application and API v1 variable documented with inline comments;BESTICON_URLcomment clarifies that an unset value ignores the sidecar (built-in scraper only) while the bundled compose stack expectshttp://besticon:8080.v1 API manifest gathering (
src/apiScraper.js) — uses sharedloadManifestIconCandidates(including manifest discovery when homepage HTML is empty); removes inline cheerio manifest parsing.API docs header (
api.html) — site logo is now a link to/(homepage); minor copy tweak ("cached for 7 days" vs "cached on disk for 7 days")./services/resolve/:service— when the path looks like a hostname (contains.), runsserviceSlugFromDomainbefore catalog matching so domain searches resolve the same slug the v1 API uses (e.g.drive.google.com→google-drive).Web UI domain service-slug derivation (
index.html) —deriveServiceSlug()mirrors the server rules (deriveServiceSlugFromDomain+ suite-parent compound slugs) instead of always taking the first domain label.Product renamed to FaviconAPI (was "MAFL+ Favicon API")
- User-facing branding updated across
index.html,api.html,src/index.js(/opensearch.xmlShortName,/robots.txtcomment),README.mdand thedocs/guides. <title>, metaapplication-name, Open Graph / Twitter Card tags and JSON-LDnamenow use FaviconAPI;alternateNameis Favicon API.- Browser shortcuts renamed: custom search engine FaviconAPI (was "MAFL+ Favicon"); bookmarklet FaviconAPI Copy (was "Mafl+ Favicon Copy"); Tools offcanvas / search-engine modal copy refers to FaviconAPI instead of MAFL+.
- External MAFL+ project links, Docker service names (
maflplus-favicon-api) and GitHub repo URLs are unchanged. - Root
favicon.pngandlogo.pngremoved fromsrc/public/; tab/bookmark icons load from/favicons/.... Open Graph / Twitter Card / JSON-LDimagefields,/robots.txtallow-list and/opensearch.xmlstill reference/logo.pngand/favicon.png.
- User-facing branding updated across
Web UI — logo and footer (
index.html,api.html)- Header logo switched from
/logo.pngto/logo.svg. - Footer redesigned: light-gray FaviconAPI brand label (Georgia serif, matching the page title) above two subtle GitHub text links — MAFL+ (
R0GGER/maflplus) and FaviconAPI (R0GGER/maflplus-favicon-api) — each with a small GitHub icon. The brand label links to faviconapi.com (opens in a new tab). Replaces the earlier "MAFL+ Favicon API · GitHub · Wiki" line. - Top navigation: MAFL+ and Favicon API GitHub links removed from both pages; Tools, Home, API and Wiki remain.
- Service icon card titles:
selfh.st/icons (cdn)→selfh.st/icons;dashboardicons.com (cdn)→dashboardicons.com; new third card titledlobehub.com. - selfh.st SVG-first fetch — when the selfhst catalog marks an icon as
SVG=Yes,/sh/:servicenow tries the jsDelivr SVG before the PNG and rasterizes it to 128×128 PNG for consistent output (fixes stale/wrong PNGs such astwake-drive, where the PNG still showed a Synology-style icon while the SVG has the correct Twake branding).
- Header logo switched from
Web UI — mobile layout refinements (
index.html,api.html)- Top navigation at ≤700px shows only Home, API and Wiki; the Tools offcanvas button is hidden so the header stays readable on narrow screens.
- Homepage Try: quick links reduced on mobile to
github.com,proton.me,immich,jellyfin(reddit.comandfirefoxremain visible on desktop). - API docs page: the mode banner (
#api-mode-banner) is hidden on mobile (≤700px) via CSS; whenAPI_REQUIRE_KEY=falseit is also hidden on desktop. - API playground Try: chips reduced on mobile to
github.com,proton.me,hosthatch.com(reddit.comandnetflix.comremain visible on desktop).
Web UI — service icons section divider (
index.html) — when searching a domain with Also include service icon lookups enabled, a subtle light-gray horizontal rule with centred Service icons label now separates the favicon provider cards from the selfh.st / dashboardicons.com / lobehub.com cards. The divider appears only after at least one service-icon card has loaded a result; it stays hidden while spinners are active and when all three providers return no icon (cards are hidden viahideOnError). Also hidden for pure service-name searches and when the checkbox is off.API docs — public mode banner (
api.html) — whenAPI_REQUIRE_KEY=false, the header no longer shows a green "Public / anonymous" badge;renderApiMode()hides the entire#api-mode-bannerso the docs page does not advertise anonymous access on instances that simply do not require keys.parseIconCandidatesFromHtml(src/providers.js) now also reports therelattribute per candidate ({ href, sizes, type, rel }), enabling the newapiScraper.jsto classify candidates by source type (svg/manifest/apple-touch-icon/png/ico). Existing callers (buildScraperCandidatesinfetchScraper) ignore the new field and are not affected.src/providers.jsexports expanded withfetchScraperPage,parseIconCandidatesFromHtml,fetchManifestIconsandparseSizesAttrso the newapiScraper.jscan reuse the existing HTML/manifest fetch pipeline without duplication. Pre-existing exports are unchanged.Dockerfileupdates for the v1 API:- Deps stage installs
python3 make g++as a virtual.build-depsapk package and removes them again afternpm ci, so native modules likebetter-sqlite3can compile from source on Alpine/musl when no prebuilt binary is available for the target arch; the runtime image is unaffected. - Runtime stage now also copies
scripts/so thenpm run keys:*CLI is available inside the container (docker compose exec maflplus-favicon-api npm run keys:create -- ...).
- Deps stage installs
Best-pick (
/{domain}) now scrapes the source site first, falling back to network providers only when scraping does not yield a usable icon — typically improving icon quality and resilience for self-hosted/private domains.Best-pick (
/{domain}) races providers in parallel instead of trying them strictly sequentially.- The preferred provider (first in priority order, typically
DEFAULT_PROVIDER) is started immediately; the remaining providers start afterPICK_HEAD_START_MS(default 150 ms) — or sooner if the preferred provider has already failed. - The first successful response wins via
Promise.any. Cache-miss latency for/{domain}drops from "worst-case ~5–10 s across providers" to "roughly the fastest provider's response time". DEFAULT_PROVIDERis still honoured under good network conditions thanks to the head-start, but a slow or failing favorite no longer blocks the response.
- The preferred provider (first in priority order, typically
HTML scraper probes candidates in parallel (
probeScraperCandidatesinsrc/providers.js).- Variant groups (sorted
NxNCDN paths) are processed concurrently, while the sequential size-jump heuristic inside each group is preserved. - Loose candidates are probed in parallel batches (default 4 at a time, via
SCRAPER_PROBE_BATCH_SIZE).
- Variant groups (sorted
MEMORY_CACHE_MAXdefault raised from500to2000entries per worker — higher in-memory cache hit ratio for deployments serving many distinct domains. Memory cost is per-worker (each cluster worker has its own LRU).Node process sets
dns.setDefaultResultOrder('ipv4first')at startup; Docker image passes--dns-result-order=ipv4firstfor additional IPv4 preference on upstream DNS resolution./{domain}/jsonscrapersourcefield — reports the cached upstream asset URL when available, instead of alwayshttps://{domain}/.Static
index.htmlis served withCache-Control: no-cacheso UI fixes apply immediately after redeploy without a hard browser refresh.
Changed
- v1 API output size: 256×256 → uniform 128×128 (
src/imageNormalize.js,src/apiRoutes.js)TARGET_SIZEandMIN_SOURCE_SIZEare both128. Every successful/api/v1/faviconresponse and cached CDN PNG is exactly 128×128; the JSONwidth/heightfields are always128.ensureExactSize()verifies the normalised buffer before caching;fit: containwith a transparent background preserves aspect ratio inside the 128×128 canvas.- SVG rasterisation density lowered from
384to192to match the 128px target. rasterizeSvgToSize(buffer, size)exported for reuse by service-icon providers (selfh.st, lobehub) that rasterize upstream SVGs at arbitrary sizes.
- v1 API source selection (
src/apiScraper.js)- ICO tier removed — root
/favicon.icoand<link rel="icon" href="…ico">candidates are no longer used; they typically contain 16–48px frames that produced blurry upscales. - Minimum source size enforced at fetch time — raster candidates smaller than 128×128 are skipped in
tryCandidateso the tier loop can continue to larger variants or the next tier. - Prefer sources > 128px — walks all tiers looking for a source whose largest dimension exceeds 128px before accepting an exactly-128px fallback anywhere in the pipeline.
- Manifest icon threshold —
fetchManifestIconsaccepts icons with no declaredsizes(probed later) and only skips entries that explicitly declare a size below 128px (was ≥ 512 only, then ≥ 128).
- ICO tier removed — root
- v1 API cache invalidation (
src/apiRoutes.js) — entries whose.meta.jsonor on-disk PNG dimensions are not 128×128 are ignored (treated as a cache miss) so older 256×256 or variable-size outputs are regenerated automatically after deploy, without manual cache clearing.
Fixed
Bot scanner paths polluting the disk cache — the catch-all
GET /{domain}route (and all/{provider}/…domain routes) previously accepted any string containing a dot as a hostname, so internet scanners probing paths like/wp-login.php,/phpinfo.php,/.envor/README.mdtriggered upstream favicon fetches and wrote thousands of junk entries under/cache. New shared modulesrc/domainValidation.jscentralises hostname checks forsrc/index.jsandsrc/apiRoutes.js: hostnames must matchSAFE_DOMAIN_REand must not end in a known file/config extension (.php,.env,.md,.json,.html,.ini,.yaml,.sql,.bak,.properties,.amplifyrc, …; any suffix starting withphpis also rejected). Invalid inputs now return400 Invalid domain/400 invalid_urlimmediately — no upstream work, no cache write. Note: existing junk files on disk are not removed automatically; clear them manually or rely onCACHE_SIZE_MBeviction.HTML scraper missed manifest icons on SPA / bot-interstitial homepages (e.g.
ah.nl) — when HTML lacks a<link rel="manifest">, the scraper now walksLinkheaders, well-known manifest paths, icon-directory heuristics andSTATIC_MANIFEST_HINTSbefore giving up. The v1 API manifest tier benefits from the same pipeline vialoadManifestIconCandidates.HTML scraper
NxNvariant ladder on/favicon/paths —faviconVariantGroupAllowsLargeJump()keeps probing consecutive sizes under/favicon/or/favicons/(e.g. Reddit 192 → 512 in the same folder) while the existingMAX_FAVICON_SIZE_JUMPguard still blocks unrelated marketing art on other URL patterns.Light and dark service-icon variants were swapped — selfh.st and dashboardicons name upstream PNGs by icon tone (
-light= pale icon,-dark= dark icon) while the API and web UI variants name the target background theme (light = darker icon for light backgrounds; dark = lighter icon for dark backgrounds).pngVariantSuffix()insrc/providers.jsnow maps APIlight→-darkanddark→-lightfor/shand/di; LobeHub synthesized variants recolor to black forlightand white fordark. Web UI preview backgrounds follow the theme (light = white, dark = charcoal). Cache keys bumped so stale mis-mapped PNGs are not reused ({variant}_v3on/shand/di;{size}_{variant}_v3on/lb)./{app-name}/jsonand/{domain}/jsonlisted providers and variants that do not exist upstream — e.g.albert-heijnonly in dashboardicons still produced selfh.st / lobehub blocks with bogus slugs; dashboardicons / selfh.st listedlightanddarkproxy/source pairs when the CDN has no-light/-darkPNG. Resolution no longer falls back to the raw input slug when a catalog has zero matches (pickResolvedSluginsrc/serviceAliases.js);resolveServiceSlugForProviderSyncvalidates slugs against each catalog before returning them. Variant availability for selfh.st and dashboardicons is confirmed via CDN probe (getSelfhstVariantAvailability/getDashboardIconsVariantAvailabilityinsrc/providers.js).HTML Scraper empty preview on ICO fallbacks (e.g.
word.office.com, Play Store favicons) — many upstream.icofiles use BMP frames that sharp cannot decode (Input buffer contains unsupported image format). Besticon still reported them in/{domain}/json→endpoints.scraper.icons(e.g. 128×128), butprobeScraperCandidatesrejected every candidate, so/s/{domain}returned 502 and the web UI showed an empty beige placeholder with dimensions but no image./s-assetreturned the rawimage/x-iconbytes, which browsers often refuse to paint in<img>. New helpers insrc/imageNormalize.js:readImageDimensions(sharp first,decode-icofallback) andtoDisplayPng(largest ICO frame → PNG at native size, no 128px minimum). Used by scraper probing (probeOne,probeIconMetadata),/s/{domain}output,/s-asset(cached under provider keyasset-v2so stale raw-ICO entries are not reused), and v1tryCandidateinsrc/apiScraper.js. The HTML Scraper card now falls back to/s-asset?url=…when the primary/s/{domain}load fails.Wrong service icon for Google Workspace subdomains (e.g.
drive.google.com) — v1 API, best-pick and service-icon cards previously derived the slug from only the first domain label (drive), which matched dashboardicons' genericeu-drivefolder icon instead of Google Drive. Slug derivation now maps suite subdomains to{parent}-{service}(with explicit rows indomainIconTags.js). Note: existing v1 API disk-cache entries are not invalidated automatically — use?refresh=1or/_internal/cache/purgeafter deploy.Fuzzy slug prefix false positives (dashboardicons / all catalogs) —
fuzzyPartScoreno longer treats a query as matching a slug segment merely because the query starts with that segment (e.g.baseten→baseinpvy-base). Prefix matches now require a hyphen boundary after the segment (gitlab-ce→gitlabstill matches). Prevents unrelated dashboardicons hits when a search term merely shares a substring with a slug part.selfh.st wrong icon for dashboard-only slugs (e.g.
eu-drive) — the selfh.st "Alternative matches" panel no longer merges dashboardicons slugs into its candidate list, so entries likeeu-driveandsynology-driveonly appear under dashboardicons.com./sh/eu-driveno longer falls back todrive-synologywhen the upstream PNG is missing (404) — combined with the hyphenated-slug guard, unknown selfh.st slugs return "icon not available" instead of a substituted icon.lobehub.com tiny icons and light/dark preview — upstream SVGs declare a 16×16 intrinsic size;
/lb/:servicenow rasterizes them to PNG at?size=64|128|256(default 128). Light/dark variants are synthesized by recoloring non-transparent pixels after rasterization. Cache keys bumped ({size}_{variant}_v2, later_v3after light/dark semantics fix) so stale pre-fix PNGs are not reused.Yandex 1×1 placeholder icons — When Yandex has no favicon for a domain it returns a 1×1 transparent PNG.
fetchYandex(src/providers.js) now rejects images ≤1×1 viasharpmetadata so/y/{domain}returns 502 and the provider is skipped in best-pick. The web UI Yandex card is hidden when the loaded image is ≤1×1 (covers stale cache entries that predate the backend check).HTML Scraper resolution mismatch on first load — the web UI no longer runs
loadFavicon()in parallel with the/{domain}/jsonicon-list fetch. Previously the size-button strip could render with the largest variant selected (e.g. 512) while the meta row still showed the dimensions from an earlier/s/{domain}image load (e.g. 192×192), becauseloadScraperSizes()rendered the buttons but never calledloadScraperVariant(0)to sync the image and footer. The scraper card now waits for the JSON icon list, then loads variant 0 vialoadScraperVariant()so the active button, displayed image and{w}×{h}meta always come from the same entry; falls back to the legacyloadFavicon()path when the JSON fetch fails or returns no icons. The primary (largest) variant's meta row again shows and copies the proxy URL ({origin}/s/{domain}).HTML Scraper size-button strip on production / datacenter hosts —
/{domain}/jsonnow augments besticon's discoveries with the existingSTATIC_CDN_HINTS+expandSizedVariantsladder before returningendpoints.scraper.icons, andfetchScraperdoes the same when picking the "best" icon for/s/{domain}. Previously, when the sidecar besticon was blocked by an origin's datacenter-IP filter (Reddit serves a JS-challenge interstitial to cloud IPs), besticon would only surface the 3 standard fallback URLs (favicon.ico,apple-touch-icon.png,apple-touch-icon-precomposed.png), so the UI's size-button strip rendered just32 / 57 / 128— even though our own node process could reach the CDN-hosted hi-res variants directly. The merged probe now lifts that to the full ladder (e.g.64 / 76 / 120 / 128 / 152 / 180 / 192 / 256 / 384 / 512forreddit.com, depending on which variants the CDN actually serves) regardless of whether besticon could see them. Implemented via newderiveHintCandidates+fetchScraperAllIconshelpers insrc/providers.js; results are LRU-cached in-memory (configurable viaSCRAPER_ICONS_CACHE_TTL, default 3600 s, andSCRAPER_ICONS_CACHE_MAX, default 500 domains) so the UI does not re-probe 8+ candidate URLs on every page load.HTML scraper Reddit regression —
<link rel="icon">,rel="shortcut"(incl. combinedrel="icon shortcut") andrel="fluid-icon"are recognised again, alongsideapple-touch-icon/-precomposed.NxNCDN paths are once again expanded to larger size variants (e.g.64x64.png→ 128/152/180/192/256/384/512). Reddit's datacenter-IP interstitial only declares a singlerel="icon shortcut" sizes="64x64", so the previous tightening caused/s/reddit.comto fall back to the old low-resapple-touch-icon.pnginstead of the modern 192×192 chat-bubble logo. TheMAX_FAVICON_SIZE_JUMP = 2.5guard still prevents Reddit's 512×512 marketing PNG from winning over the real 192×192 favicon.STATIC_CDN_HINTSforreddit.com/www.reddit.comis restored as a defensive fallback when the interstitial drops the icon link entirely.HTML scraper on VPS/datacenter hosts — upstream fetches now use a dedicated IPv4-only undici dispatcher (
src/upstreamFetch.js), fixing broken IPv6 egress that caused CDN assets (e.g.redditstatic.com) to fail while same-origin fallbacks still worked.HTML scraper icon probing — CDN/cross-origin icon URLs are fetched with a bare
upstreamFetch(no extra headers) before retrying with browser-like headers; extra headers were rejected by some CDNs from datacenter IPs.HTML scraper homepage fetch — retries across
https://{domain}/andhttps://www.{domain}/with bare, HTTP/1.1, curl and Chrome user-agents when the initial HTML request fails or returns an empty body.HTML scraper fallback ordering — standard
/apple-touch-icon-precomposed.pngfallbacks are only used when all HTML/CDN candidates fail, preventing a wrong 128×128 icon from winning over a reachable CDN hi-res variant.HTML scraper static CDN hints — when homepage HTML is blocked entirely (e.g.
reddit.comfrom datacenter IPs), known CDN entry points are probed and expanded to largerNxNvariants (STATIC_CDN_HINTSinproviders.js).HTML scraper variant probing — when probing
NxNCDN paths, stops before a sharp size jump (e.g. Reddit serves a full-body marketing PNG at 512×512 while 64–192 are the actual logo favicons). Revised: the largest hit in a variant group is no longer dropped when its URL is under/favicon/or/favicons/— so legitimate hi-res favicon folders (e.g. Reddit 512×512) appear inendpoints.scraper.iconsand the size-button strip.HTML scraper mask-icon exclusion — ignores Safari
rel="mask-icon"/safari-pinned-tab.svgassets; these are monochrome pinned-tab silhouettes, not display favicons (e.g.proton.meshowed a solid black "P" instead of the purple gradient logo).HTML scraper manifest monochrome icons — skips web-app-manifest icons with
purpose: monochrome(and known monochrome URL patterns); sites like YouTube expose a white logo at 512×512 alongside the red favicon set.HTML scraper manifest size threshold — only manifest icons with a declared size of
512×512or larger are considered, avoiding low-res manifest entries winning over a higher-qualityapple-touch-icon. Superseded: layered manifest discovery + monochrome filtering now allow declared sizes ≥ 128 while still skipping tiny manifest entries when a size is explicitly declared below 128.HTML Scraper fast-proxy meta dimensions — selecting the lavender proxy button could show
256×256(or other native sizes) in the footer when/s/disk cache predated a lowerSCRAPER_MAX_ICON_SIZE; meta now reports the configured cap and auto-retries with?refresh=1once when the loaded image is still larger.Web UI HTML Scraper URL — card no longer showed
https://{domain}/under the icon; it now always displays and copies the proxy URL ({origin}/s/{domain}) when the fast-proxy variant is active, matching how other providers expose usable API URLs.
Internal
src/domainValidation.js— sharedextractDomainFromInput,extractDomainFromUrl,isValidHostnameandBLOCKED_FILE_EXTENSIONSblocklist; consumed bysrc/index.js(extractDomain) andsrc/apiRoutes.js(/api/v1/favicon,/cdn/favicons/{domain}.png).src/providers.jsexportsinvalidateScraperDomainCaches(domain)— evicts the in-memory scraper page/icons/besticon LRU entries for one domain and drops the corresponding disk discovery files whenSCRAPER_DISK_CACHEis enabled.- New dependencies:
better-sqlite3(synchronous SQLite driver in WAL mode so the API key store is safe across cluster workers writing into the same/cache/api-keys.sqlitefile) anddecode-ico(multi-frame ICO decoder retained for legacy code paths; the v1 API no longer selects ICO sources). - The v1 API cache is namespaced separately from the existing provider-based cache (
src/cache.js): PNGs live at${API_CACHE_DIR}/{domain}.pngwith a sibling.meta.json({ sourceType, width, height, cachedAt }) so the two schemes can coexist in/cachewithout key collisions. The existingCACHE_SIZE_MBLRU rescan picks up${API_CACHE_DIR}/...files alongside the rest of the cache directory, so the global disk-size cap also covers the v1 API output. - New
fetchScraperAllIcons(domain)exported fromsrc/providers.jsis the single source of truth for the merged scraper icon list (besticon + static CDN hints + sized variants, deduped + sorted by area). Backed by an in-memoryLRUCache(SCRAPER_ICONS_CACHE_MAX/SCRAPER_ICONS_CACHE_TTL). Consumed by/{domain}/json(endpoints.scraper.icons);fetchScraperuses the samederiveHintCandidateshelper to enrich its candidate pool beforeprobeScraperCandidatesso the chosen "best" icon matches the largest entry the UI displays.probeScraperCandidatesis invoked withlimit=32in the besticon path so the augmented pool is not truncated at the previous default of 16.fetchBesticonAllIconsremains a public export for external callers. discoverManifestUrls,loadManifestIconCandidatesandgetScraperMaxIconSizeexported fromsrc/providers.js;fetchScraperPagenow also returns the raw HTTPLinkheader for manifest discovery.fetchScraperAssetis exported fromsrc/providers.js; the asset fetcher is reused by the/s-assetroute.resolveSelfhstSlugSyncre-exported fromsrc/serviceAliases.js.readImageDimensions,toDisplayPngandlooksLikeIcoexported fromsrc/imageNormalize.jsfor ICO-aware probing and browser-safe PNG serving.- New
besticonIconsToCandidateshelper bridges besticon's raw/allicons.jsonresponse to the existing{ href, sizes, type }candidate format consumed byrankCandidates/probeScraperCandidates. BESTICON_URLis read once at module load (process.env.BESTICON_URL) and trailing slashes are stripped so bothhttp://besticon:8080andhttp://besticon:8080/work.services.txtadded to.gitignore(local notes file).cache.del()added for per-entry cache invalidation (used by/s/{domain}?refresh=1).- HTML scraper upstream requests routed through
src/upstreamFetch.jsinstead of globalfetch. - Disk cache metadata now persists the upstream
urlfor scraper entries (used byX-Favicon-Urland/{domain}/json). - New
runInBatcheshelper insrc/providers.jsfor bounded-parallel iteration withPromise.allSettledsemantics. - Disk cache directory (
/cache) is shared across cluster workers; each worker keeps its own in-process LRU memory cache.
[1.0.0] — Initial public release
Added
- Express-based favicon proxy with two-tier (memory + disk) caching.
- Domain-based providers:
- Google (
/g/{size}/{domain}) — sizes 16, 32, 64, 128. - DuckDuckGo (
/d/{domain}). - Yandex (
/y/{domain}). - Favicon.so (
/f/{domain}). - Vemetric (
/v/{domain}) with optional?size=and?format=png|jpg|webp. - Favicon-3j1 (
/p/{domain}).
- Google (
- Best-pick endpoint (
/{domain}) cascading through all providers and scoring results withsharp. /{domain}/jsonendpoint listing every favicon URL for a given domain.- Web UI (
src/public/index.html):- Per-provider cards with click-to-copy URL behaviour.
- Top navigation bar linking to MAFL+, the Favicon API repo and the Wiki.
- Bookmarklet ("Mafl+ Favicon Copy") that copies the favicon URL of the current page.
- "Show source" indicator displaying which upstream provider returned the favicon.
- Configuration via environment variables:
PORT,CACHE_DIR,MEMORY_CACHE_MAX,MEMORY_CACHE_TTL,DISK_CACHE_TTL,UPSTREAM_TIMEOUT. - Dockerfile,
docker-entrypoint.shanddocker-compose.ymlfor container deployment, plus a GitHub Actions workflow that publishes images toghcr.io/r0gger/favicon-api:latest. .gitattributesenforcing LF line endings.