location = /favicon.ico {
alias /usr/share/nginx/html/favicon.ico;
}
Adding a Favicon for Blobs and Other Non-HTML Resources
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.
What 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
Prerequisites
- Blueprint Workspace
- Knowledge of CAE handlers and Spring configuration
- Access to the configuration of the reverse proxy in front of the CAE
Target Audience
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.ftlof thepagebrick links thefaviconblob property of the site’s root channel, if set. -
Themes usually overwrite
Page._favicon.ftland link the favicon files they ship themselves. For example,shared-example-themelinksimg/favicon.icoand further icon variants withbp.getLinkToThemeResource(…), independent of the root channel’sfaviconproperty.
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 |
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 |
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
/blueprintand the Spring MVC servlet path/servlet. Handler URLs therefore become/blueprint/servlet/…behind the proxy. -
favicon.icois already listed incae.site.ignore-prefix-patternsof the Blueprint CAE applications, so a request for/blueprint/servlet/favicon.icodoes 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:
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 |
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:
-
The
faviconblob property of the site’s root channel (CMNavigation), if set. This optional override lets a single site deviate from its theme. -
Otherwise, the
datablob ofimg/favicon.icoin the folder of the root channel’s theme.
|
The standard Blueprint Studio has no form field for the Also note that themes that overwrite |
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 |
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 |
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.icowith a non-emptydatablob. Not every theme ships a favicon. Alternatively, check whether the root channel has afaviconblob. -
For approach C, check that the content at the configured path exists and has a non-empty
datablob. -
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
@Beanmethod declares the concrete handler type.