Adding a Favicon for Blobs and Other Non-HTML Resources

Last updated 10 minutes ago

Learn how to deliver /favicon.ico for your sites, so that browsers also show your favicon when an image, PDF, or other blob is opened directly instead of an HTML page.

LightbulbWhat you will learn

  • Understand why browsers request /favicon.ico for non-HTML resources
  • Choose between a static and a content-based approach
  • Implement a CAE handler that delivers the favicon of a site's theme
  • Route /favicon.ico through your reverse proxy

Person reading a bookPrerequisites

  • Blueprint Workspace
  • Knowledge of CAE handlers and Spring configuration
  • Access to the configuration of the reverse proxy in front of the CAE

Person in front of a laptopTarget Audience

This guide is for Developers, Administrators.

HTML pages rendered by the CAE declare their favicon in the page head. In the Blueprint, Page._head.ftl includes the partial template Page._favicon.ftl:

  • The default Page._favicon.ftl of the page brick links the favicon blob property of the site’s root channel, if set.

  • Themes usually overwrite Page._favicon.ftl and link the favicon files they ship themselves. For example, shared-example-theme links img/favicon.ico and further icon variants with bp.getLinkToThemeResource(…​), independent of the root channel’s favicon property.

The typical way to define the favicon of a site is therefore through its theme.

Images, PDFs, and other blobs that the browser displays directly have no HTML head and therefore no way to point to a favicon. For these resources, browsers fall back to the root of the host and request /favicon.ico. If that request fails, the browser shows a generic icon.

This guide explains how to make /favicon.ico available for your sites.

There is no way to declare a favicon per blob. In particular, the HTTP response header Link: </icon.png>; rel="icon" is not supported by the major browsers. The only reliable solution is to answer requests for /favicon.ico on every host that delivers blobs.

Choosing an approach

Approach Pros Cons

A: Static file delivered by the reverse proxy or the CAE

Simple, no Java code, very fast

One icon per proxy rule, maintained outside the content repository; changes require a deployment

B: Per-site CAE handler (recommended)

Delivers the img/favicon.ico of the site’s theme, which HTML pages already use; one icon per site; no Studio customization required; optional override per site by the root channel’s favicon property

Requires Java code and one proxy rule per host

C: Global CAE handler for a fixed content path

Icon maintained as content; works without per-host proxy rules

Only one icon for all sites served by the CAE

All approaches have the following in common:

  • The CAE is deployed with the context path /blueprint and the Spring MVC servlet path /servlet. Handler URLs therefore become /blueprint/servlet/…​ behind the proxy.

  • favicon.ico is already listed in cae.site.ignore-prefix-patterns of the Blueprint CAE applications, so a request for /blueprint/servlet/favicon.ico does not trigger site resolution.

  • Browsers cache favicons aggressively and independently of the regular HTTP cache. When testing, request the URL directly (for example, with curl) instead of relying on the icon shown in the browser tab.

Approach A: Deliver a static file

If all your sites share the same icon, deliver a static file.

The simplest option is to let the reverse proxy serve a file from its own file system, without involving the CAE at all:

location = /favicon.ico {
    alias /usr/share/nginx/html/favicon.ico;
}

Alternatively, package the icon with your CAE. The Blueprint CAE applications set server.servlet.register-default-servlet=true, so the servlet container delivers files below META-INF/resources/ of any JAR on the classpath. Put the icon into a CAE extension module, for example:

<your-cae-extension>/src/main/resources/META-INF/resources/static/favicon.ico

The CAE then delivers it as /blueprint/static/favicon.ico. Forward requests for /favicon.ico there:

location = /favicon.ico {
    proxy_pass http://cae/blueprint/static/favicon.ico;
    proxy_set_header Host $http_host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

Use the exact match location = /favicon.ico instead of the prefix match location /favicon.ico, which also matches paths such as /favicon.ico.bak.

Approach B: Deliver the site’s favicon with a CAE handler

Each Blueprint site already has a favicon provided by its theme. Themes ship it as src/img/favicon.ico, and the theme importer stores it as a CMImage document img/favicon.ico with a data blob, next to the CMTheme document in the theme folder, for example, /Themes/<theme-name>/img/favicon.ico. This is the same document that bp.getLinkToThemeResource('img/favicon.ico') links in the theme’s Page._favicon.ftl.

A small handler can deliver this blob for /favicon.ico. HTML pages and blobs then show the same icon, and the icon is maintained where frontend developers maintain all other theme resources. Importing a new theme version updates it.

The handler follows the pattern of the Blueprint RobotsHandler and LlmsHandler. It answers an internal URL that contains the root segment of the site, for example, /service/favicon/corporate. The reverse proxy maps /favicon.ico of each host to this URL. The handler determines the favicon as follows:

  1. The favicon blob property of the site’s root channel (CMNavigation), if set. This optional override lets a single site deviate from its theme.

  2. Otherwise, the data blob of img/favicon.ico in the folder of the root channel’s theme.

The standard Blueprint Studio has no form field for the favicon property of channels. Editors can only set it after you add a field to the channel forms, or with an import or a Unified API client. That is why the theme favicon is the recommended source: it requires no Studio customization. If you do not need the override, omit the first step of the lookup.

Also note that themes that overwrite Page._favicon.ftl, such as the ones based on shared-example-theme, ignore the favicon property of the root channel in HTML pages. If you use the override, HTML pages and /favicon.ico may show different icons for these themes.

Step 1: Implement the handler

package com.example.cae.favicon;

import com.coremedia.blueprint.cae.handlers.HandlerBase;
import com.coremedia.blueprint.cae.handlers.NavigationSegmentsUriHelper;
import com.coremedia.blueprint.common.contentbeans.CMNavigation;
import com.coremedia.blueprint.common.contentbeans.CMTheme;
import com.coremedia.cap.common.Blob;
import com.coremedia.cap.common.CapBlobRef;
import com.coremedia.cap.common.CapPropertyDescriptor;
import com.coremedia.cap.common.CapPropertyDescriptorType;
import com.coremedia.cap.content.Content;
import com.coremedia.objectserver.web.HandlerHelper;
import com.coremedia.objectserver.web.UserVariantHelper;
import com.coremedia.objectserver.web.cachecontrol.CacheControlStrategy;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.ModelAndView;

/**
 * Delivers a site's favicon. For example, {@code /service/favicon/corporate} serves the favicon blob of
 * the site's root channel, if set, or else the {@code img/favicon.ico} of the site's theme.
 * A reverse proxy maps {@code /favicon.ico} of each host to this URL.
 */
@Controller
public class SiteFaviconHandler extends HandlerBase {

  private static final String URI_PATTERN = "/service/favicon/{root}";
  private static final String THEME_FAVICON_PATH = "img/favicon.ico";
  private static final String THEME_RESOURCE_BLOB_PROPERTY = "data";

  private final NavigationSegmentsUriHelper navigationSegmentsUriHelper;

  public SiteFaviconHandler(NavigationSegmentsUriHelper navigationSegmentsUriHelper) {
    this.navigationSegmentsUriHelper = navigationSegmentsUriHelper;
  }

  @GetMapping(URI_PATTERN)
  public ModelAndView handleFavicon(@PathVariable("root") String segment,
                                    WebRequest webRequest,
                                    HttpServletRequest request,
                                    HttpServletResponse response) {
    CMNavigation rootNavigation = navigationSegmentsUriHelper.lookupRootSegment(segment);
    if (rootNavigation == null) {
      return HandlerHelper.notFound("Unknown root segment " + segment);
    }

    CapBlobRef blob = getFavicon(rootNavigation, request);
    if (blob == null) {
      return HandlerHelper.notFound("No favicon for " + segment);
    }

    if (webRequest.checkNotModified(blob.getETag())) {
      // expose the model, so that the CacheControlStrategy can compute the TTL of the 304 response
      webRequest.setAttribute(CacheControlStrategy.NOT_MODIFIED_ROOT_MODEL_KEY, blob, WebRequest.SCOPE_REQUEST);
      return null;
    }

    return doCreateModelWithView(true, blob, null, null, response);
  }

  private static CapBlobRef getFavicon(CMNavigation rootNavigation, HttpServletRequest request) {
    // optional override: the favicon of the root channel
    Blob favicon = rootNavigation.getFavicon();
    if (favicon instanceof CapBlobRef blob) {
      return blob;
    }
    // the developer selects a theme variant in the preview, as for HTML pages
    return getThemeFavicon(rootNavigation.getTheme(UserVariantHelper.getUser(request)));
  }

  private static CapBlobRef getThemeFavicon(CMTheme theme) {
    if (theme == null) {
      return null;
    }
    // theme resources are located relative to the folder of the theme document
    Content themeFolder = theme.getContent().getParent();
    Content resource = themeFolder == null ? null : themeFolder.getChild(THEME_FAVICON_PATH);
    if (resource == null || !resource.isDocument()) {
      return null;
    }
    CapPropertyDescriptor descriptor = resource.getType().getDescriptor(THEME_RESOURCE_BLOB_PROPERTY);
    if (descriptor == null || descriptor.getType() != CapPropertyDescriptorType.BLOB) {
      return null;
    }
    return resource.getBlobRef(THEME_RESOURCE_BLOB_PROPERTY);
  }
}

The handler returns the CapBlobRef as the model, just like the Blueprint CapBlobHandler does. The CAE writes the blob with its stored content type, and conditional requests with If-None-Match are answered with 304 Not Modified. Sites without any favicon and unknown segments are answered with 404 Not Found, so browsers fall back to their default icon.

Unlike the CapBlobHandler, this handler does not call validationService.validate(…​), so the favicon document’s validity dates are not checked. This is harmless for theme resources, which are not subject to a publication workflow with validity dates.

CMNavigation.getTheme resolves the theme like HTML rendering does, including the theme variants of developers in the preview. If your theme ships its favicon at a different path, adjust THEME_FAVICON_PATH to the path that your Page._favicon.ftl passes to bp.getLinkToThemeResource.

Step 2: Register the handler as a Spring bean

HandlerBase requires some collaborators. Set them with BlueprintHandlersCaeBaseLibConfiguration.configureHandlerBase. Otherwise, the handler fails during initialization with IllegalStateException: Required property not set.

package com.example.cae.favicon;

import com.coremedia.blueprint.base.links.ContentLinkBuilder;
import com.coremedia.blueprint.base.links.UrlPathFormattingHelper;
import com.coremedia.blueprint.cae.handlers.NavigationSegmentsUriHelper;
import com.coremedia.mimetype.MimeTypeService;
import com.coremedia.objectserver.dataviews.DataViewFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import static com.coremedia.blueprint.cae.config.BlueprintHandlersCaeBaseLibConfiguration.configureHandlerBase;

@Configuration(proxyBeanMethods = false)
public class FaviconCaeConfiguration {

  @Bean
  public SiteFaviconHandler siteFaviconHandler(NavigationSegmentsUriHelper navigationSegmentsUriHelper,
                                               MimeTypeService mimeTypeService,
                                               UrlPathFormattingHelper urlPathFormattingHelper,
                                               DataViewFactory dataViewFactory,
                                               ContentLinkBuilder contentLinkBuilder) {
    SiteFaviconHandler handler = new SiteFaviconHandler(navigationSegmentsUriHelper);
    configureHandlerBase(handler, mimeTypeService, urlPathFormattingHelper, dataViewFactory, contentLinkBuilder);
    return handler;
  }
}

Make sure that Spring picks up the configuration class, for example, by importing it from an existing configuration of your CAE extension or by registering it in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports.

Declare the @Bean method with the concrete handler type (here SiteFaviconHandler), not with HandlerBase, and make the handler a top-level class. Spring MVC detects @Controller beans by their type. If the RequestMappingHandlerMapping is initialized before the handler bean is created, Spring only sees the declared return type of the factory method. With HandlerBase as return type, the mapping is then silently skipped, and the URL is answered with 404 Not Found. Whether this happens depends on the bean creation order, so the problem may appear only in some deployments.

Step 3: Route /favicon.ico in the reverse proxy

Map /favicon.ico of each host to the internal URL with the root segment of the site that the host delivers. This is the same setup as for robots.txt and llms.txt.

NGINX, with one server block per site:

server {
    server_name www.example.com;

    location = /favicon.ico {
        proxy_pass http://cae/blueprint/servlet/service/favicon/corporate;
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    # ... other locations of the site
}

Traefik, in the style of the Blueprint dynamic configuration under global/deployment/docker/blueprint/traefik:

http:
  routers:
    cae-live-corporate-favicon:
      # lower priority than any landing page route but higher than the servlet catch-all rule
      priority: 40
      entrypoints:
        - webaccesssecure
        - webaccessinsecure
      rule: Host(`corporate.{{ env "ENVIRONMENT_FQDN"}}`) && Path(`/favicon.ico`)
      service: cae-live
      middlewares:
        - cae-live-corporate-favicon@file

  middlewares:
    cae-live-corporate-favicon:
      replacePath:
        path: "/blueprint/servlet/service/favicon/corporate"

Add one router per host that delivers a site with its own favicon, as with localized sites such as corporate-de-de.

If your blobs are delivered from a separate host, for example, a CDN domain, that host needs a /favicon.ico rule as well.

The Preview CAE has a single host for all sites, so /favicon.ico cannot be mapped to one site there. Verify the setup on the Live CAE hosts. In the Preview CAE, you can request the internal URL, for example, /service/favicon/corporate, directly.

Approach C: Deliver one favicon from a fixed content path

If all sites share one icon, but you still want to maintain it as content, a handler can directly answer /favicon.ico with the blob of a content item at a configurable path.

The handler looks like this:

package com.example.cae.favicon;

import com.coremedia.blueprint.cae.handlers.HandlerBase;
import com.coremedia.cap.common.CapBlobRef;
import com.coremedia.cap.common.CapPropertyDescriptor;
import com.coremedia.cap.common.CapPropertyDescriptorType;
import com.coremedia.cap.content.Content;
import com.coremedia.cap.content.ContentRepository;
import com.coremedia.objectserver.web.HandlerHelper;
import com.coremedia.objectserver.web.cachecontrol.CacheControlStrategy;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.ModelAndView;

/**
 * Delivers the {@code data} blob of the content at a configured path for {@code /favicon.ico}.
 */
@Controller
public class FaviconHandler extends HandlerBase {

  private static final String BLOB_PROPERTY = "data";

  private final ContentRepository contentRepository;
  private final String faviconPath;

  public FaviconHandler(ContentRepository contentRepository, String faviconPath) {
    this.contentRepository = contentRepository;
    this.faviconPath = faviconPath;
  }

  @GetMapping("/favicon.ico")
  public ModelAndView handleFavicon(WebRequest webRequest, HttpServletResponse response) {
    Content content = contentRepository.getChild(faviconPath);
    if (content == null || content.isDeleted() || content.isDestroyed()) {
      return HandlerHelper.notFound("No favicon at " + faviconPath);
    }

    CapPropertyDescriptor descriptor = content.getType().getDescriptor(BLOB_PROPERTY);
    if (descriptor == null || descriptor.getType() != CapPropertyDescriptorType.BLOB) {
      return HandlerHelper.notFound("No blob property " + BLOB_PROPERTY + " at " + faviconPath);
    }

    CapBlobRef blob = content.getBlobRef(BLOB_PROPERTY);
    if (blob == null) {
      return HandlerHelper.notFound("Empty favicon at " + faviconPath);
    }

    if (webRequest.checkNotModified(blob.getETag())) {
      webRequest.setAttribute(CacheControlStrategy.NOT_MODIFIED_ROOT_MODEL_KEY, blob, WebRequest.SCOPE_REQUEST);
      return null;
    }

    return doCreateModelWithView(true, blob, null, null, response);
  }
}

Register it like the handler of Approach B, passing the ContentRepository and the path, for example, from a property:

@Bean
public FaviconHandler faviconHandler(@Value("${example.favicon.path}") String faviconPath,
                                     ContentRepository contentRepository,
                                     MimeTypeService mimeTypeService,
                                     UrlPathFormattingHelper urlPathFormattingHelper,
                                     DataViewFactory dataViewFactory,
                                     ContentLinkBuilder contentLinkBuilder) {
  FaviconHandler handler = new FaviconHandler(contentRepository, faviconPath);
  configureHandlerBase(handler, mimeTypeService, urlPathFormattingHelper, dataViewFactory, contentLinkBuilder);
  return handler;
}

Use a content item with a blob property data, for example, a CMImage or CMDownload with the path /Themes/favicon/favicon.ico. Make sure that the item is published, because the Live CAE reads it from the Replication Live Server.

The handler answers /blueprint/servlet/favicon.ico. If your proxy adds /blueprint/servlet to all paths that are not mapped otherwise, as the default Blueprint Traefik configuration does, /favicon.ico already reaches the handler. Otherwise, add a rule such as:

location = /favicon.ico {
    proxy_pass http://cae/blueprint/servlet/favicon.ico;
    proxy_set_header Host $http_host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

Verifying the setup

Request the favicon through the proxy and check the status and content type:

curl -sI https://www.example.com/favicon.ico

The response must have the HTTP status code 200 and a content type such as image/x-icon, image/vnd.microsoft.icon, or image/png. Then open a blob URL of the site, for example, an image below /resource/image/…​, in a new browser profile or a private window. The browser tab now shows your favicon.

If you get a 404:

  • For approaches B and C, call the CAE directly, for example, /blueprint/servlet/service/favicon/<root-segment> or /blueprint/servlet/favicon.ico, to find out whether the proxy rule or the CAE is the problem.

  • For approach B, check that the root channel links a theme, and that the theme folder contains a document img/favicon.ico with a non-empty data blob. Not every theme ships a favicon. Alternatively, check whether the root channel has a favicon blob.

  • For approach C, check that the content at the configured path exists and has a non-empty data blob.

  • For the Live CAE, check that the theme or content is published, because the Live CAE reads it from the Replication Live Server.

  • Check that the @Bean method declares the concrete handler type.

Copyright © 2026 CoreMedia GmbH, CoreMedia Corporation. All Rights Reserved.Privacy | Legal | Imprint
Loading...