Degoog Docs

Degoog Favicon providers

Write a provider that tells Degoog where to find the icon for a domain. Core fetches the icon, checks it and serves it.

degoog-cli is now available. Run create --type favicon to scaffold a favicon provider from your terminal. Install with curl -fsSL https://raw.githubusercontent.com/degoog-org/cli/main/install.sh | sh or grab it from github.com/degoog-org/cli.

What favicon providers are

Core has no built-in favicon source. Every icon on the results page comes from a favicon extension installed from the Store.

Degoog gives your provider a hostname. You return a URL, the icon bytes, or null. Core fetches and checks the icon, and the browser loads it from a signed /api/proxy/favicon URL on the same instance.

Where providers live

Installed providers go in data/favicon/, or wherever DEGOOG_FAVICON_DIR points. Each provider is a folder with an entry file named index.js, index.ts, index.mjs or index.cjs. The entry exports a class or an object with a name and a getFavicon function.

The loader looks for the default export, then provider, then Provider. It skips anything without a string name and a getFavicon function.

Provider contract

Two members are required. name is the label in Settings -> Favicons. getFavicon(host, context) is async and returns a FaviconResult. host is a bare hostname like example.com, with no scheme or path.

Return null on a miss instead of throwing. Core catches thrown errors, but it logs every one.

description, settingsSchema and configure(settings) are optional and work as they do on engines. Search engines has the SettingField reference.

export type FaviconResult =
  | { url: string }
  | { data: Uint8Array; contentType: string }
  | null;

export interface FaviconProvider {
  name: string;
  description?: string;
  settingsSchema?: SettingField[];
  configure?(settings: Record<string, unknown>): void;
  getFavicon(host: string, context: FaviconContext): Promise<FaviconResult>;
}

The three return shapes

You return What core does with it Use it for
{ url: string } Fetches the URL on the server and validates the bytes. If the fetch or the check fails, asks the next provider. Services with one icon URL per domain, like the official Google and DuckDuckGo providers.
{ data: Uint8Array, contentType: string } Validates the bytes you passed. Icons you generate, like identicons, or icons you fetched yourself with context.fetch, for example because the upstream needs an auth header.
null Asks the next provider in the list. Any host you know nothing about.

The context object

Every call to getFavicon gets a context argument.

Field Type Description
context.fetch typeof fetch Use it instead of the global fetch. It applies the proxy settings and this provider's Outgoing HTTP client.
context.userAgent string The user agent Degoog sends on outgoing requests.
context.size number The icon size core asks for, in pixels. It is 64. Pass it to services that take a size. Returning a smaller icon is fine, core keeps whatever size you return.
context.useCache UseCache context.useCache<T>(namespace, defaultTtlMs) returns an async TTL cache with get, set, delete and clear. It lives in Valkey when that is configured, in process memory otherwise. Core already caches the final icon, so use this for your own lookups, like an API token.

How core checks the bytes

  • Core accepts PNG, ICO, GIF, JPEG and WebP. It rejects SVG, because SVG can carry scripts, so convert SVG icons to PNG.
  • Core reads the format from the file's first bytes and ignores the Content-Type header, yours or the upstream's.
  • Icons over 64KB are rejected.
  • Core fetches a { url } result with a 5 second timeout and checks the resolved address on every redirect, so a URL that points at 127.0.0.1 or a private network fails.

A failed check counts as null, and core asks the next provider.

Priority and the provider chain

Core asks providers one at a time and stops at the first icon that passes validation. Unlike autocomplete, results are not merged.

The order comes from the reserved priority settings key. Higher goes first, and ties fall back to folder order. Providers with no priority yet, such as one just installed, go after all ranked ones. Instance owners set the order by dragging cards in Settings -> Favicons. Core skips providers with the reserved disabled key. Your provider doesn't need to handle either key.

When the indexer's favicon store is on, core checks it before any provider.

Each getFavicon call gets 5 seconds, set by DEGOOG_FAVICON_PROVIDER_TIMEOUT_MS. A provider that runs over counts as null.

If every provider returns null, the result shows the domain's first letter. If no provider is enabled and the favicon store is off, results show a "No favicon extension installed" placeholder and the browser requests no icons.

Core caches the final answer, icon or miss, so your provider is asked about a given host rarely, not on every search.

Outgoing transport

Every favicon provider gets an Outgoing HTTP client selector under Advanced, stored in the reserved outgoingTransport key. Options are fetch, curl, curl-fallback, or any installed transport. It applies to context.fetch only, not to core's fetch of a { url } result.

IDs, folders and the manifest

The ID, which is also the settings key, is the install folder name plus -favicon. Store installs name the folder after the repo and item, giving {author}-{repo}-{item}-favicon. The official Google provider is degoog-org-official-extensions-google-favicon.

In a store repository, providers live under favicon/<name>/ and are listed under the "favicon" key of the root package.json.

my-degoog-repo/
  package.json
  favicon/
    my-provider/
      index.js
      author.json
      README.md
      screenshots/
{
  "favicon": [
    {
      "path": "favicon/my-provider",
      "name": "My Favicon Provider",
      "description": "Icons from icons.example.com."
    }
  ]
}

author.json, README.md and screenshots/ are optional. Other extensions can depend on your provider with a dependency URL ending in favicon/<name>. Store repositories covers the rest of the manifest.

Minimal example with a URL

export default class MyFaviconProvider {
  name = "My Favicon Provider";
  description = "Icons from icons.example.com.";

  async getFavicon(host, context) {
    if (!host) return null;
    return {
      url: `https://icons.example.com/${encodeURIComponent(host)}?size=${context.size}`,
    };
  }
}

The provider only builds the URL. If it returns a 404 or something that isn't an image, core asks the next provider.

Minimal example with a generated icon

This one returns a 1x1 GIF coloured from a hash of the hostname, with no network calls.

const GIF_HEAD = [0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, 0x01, 0x00, 0x80, 0x00, 0x00];
const GIF_TAIL = [
  0x00, 0x00, 0x00,
  0x2c, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00,
  0x02, 0x02, 0x44, 0x01, 0x00,
  0x3b,
];

const hashHost = (host) => {
  let hash = 0;
  for (const char of host) hash = (hash * 31 + char.charCodeAt(0)) >>> 0;
  return hash;
};

export default {
  name: "Colour Swatch",
  description: "A tiny colour per domain, no network needed.",

  async getFavicon(host) {
    if (!host) return null;
    const hash = hashHost(host);
    const rgb = [(hash >> 16) & 0xff, (hash >> 8) & 0xff, hash & 0xff];
    return {
      data: new Uint8Array([...GIF_HEAD, ...rgb, ...GIF_TAIL]),
      contentType: "image/gif",
    };
  },
};

Generated bytes go through the same validation as fetched ones.

Settings example

export default class MyFaviconProvider {
  name = "My Favicon Provider";

  settingsSchema = [
    {
      key: "apiKey",
      label: "API Key",
      type: "password",
      secret: true,
      required: true,
      description: "Key for the icon service.",
    },
  ];

  apiKey = "";

  configure(settings) {
    this.apiKey = typeof settings.apiKey === "string" ? settings.apiKey.trim() : "";
  }

  async getFavicon(host, context) {
    if (!host || !this.apiKey) return null;
    try {
      const res = await context.fetch(
        `https://icons.example.com/v1/${encodeURIComponent(host)}`,
        { headers: { Authorization: `Bearer ${this.apiKey}`, "User-Agent": context.userAgent } },
      );
      if (!res.ok) return null;
      const contentType = res.headers.get("content-type") ?? "";
      return { data: new Uint8Array(await res.arrayBuffer()), contentType };
    } catch {
      return null;
    }
  }
}

The upstream needs an auth header, so the provider fetches the bytes with context.fetch and returns { data }. Core still reads the format from the bytes, so a wrong content-type does no harm.

Setup

Add a folder with an index.js to data/favicon/ or DEGOOG_FAVICON_DIR and restart Degoog. Then enable the provider and drag it into place in Settings -> Favicons.

The official store has Google, DuckDuckGo, Kagi, Yandex, Site favicon (experimental) and Identicon, which generates a 64px icon offline. Google and DuckDuckGo are short enough to use as references.