Serving a CMS-Managed Favicon by Extending the Sitecore Content SDK Layout Service
Hello everyone, and welcome to another blog post! In this article, I’ll walk you through how to extend the Layout Service in the Sitecore Content SDK using a real-world example. I’ll demonstrate the implementation step by step and explain how you can apply the same approach to your own Sitecore projects. I previously implemented a similar use case using the JSS SDK, which you can refer to here.
Let’s start by understanding the real-world use case. Every website needs a favicon, and in a typical Next.js project, it is usually managed through the public folder. In this article, we’ll explore how to move the site favicon out of the Next.js /public folder and manage it directly in Sitecore by extending the Layout Service in the Sitecore Content SDK. This allows content authors, developers to upload and manage the favicon once through Site Settings, while every page can access it through sitecore.context—without requiring an additional fetch for each component or a redeployment whenever the favicon changes.
Out of the box, the Content SDK starter kit renders a hardcoded in Layout.tsx. That works for a single site, but it breaks down quickly:
- Multisite: every site in the tenant shares the same rendering host, so every site shows the same icon.
- Authoring: a rebrand or seasonal icon means a code change, a PR and a deployment.
- Consistency: the logo lives in the Media Library, but its smallest sibling lives in the repo.
Why the Layout Service is the right place
In the Content SDK, every page request flows through the catch-all route pages/[[...path]].tsx, which calls client.getPage(...) for normal requests and client.getPreview(...) for editing. Both run through services owned by the SitecoreClient singleton in src/lib/sitecore-client.ts.
he SitecoreClient constructor allows you to pass an optional custom object to override core services—such as layoutService, editingService, and dictionaryService—while defaulting to the standard implementations whenever custom services are omitted.
The key detail is that the SitecoreClient constructor accepts an optional custom object alongside your sitecore.config settings. It lets you swap in your own layoutService, editingService, dictionaryService and others. If you supply one, the client uses it; if not, it falls back to the default. So the plan is:
- Add the favicon field to the Site Settings item in Sitecore.
- Create a GraphQL query to fetch the favicon.
- Extend
LayoutServiceto inject the favicon intositecoreContext. - Register the custom
LayoutServiceinsitecore-client.ts. - Render the favicon inside
<Head>inLayout.tsx. - Apply the same update to
editingServiceto support Page Builder.
Step 1: Add a Favicon field to Site Settings
In the Content Editor, I created a small base template and added it to the site's Settings template, so every site in the tenant gets the field:
- Template: /sitecore/templates/Project/{Tenant}/_Favicon
- Field: Favicon (type: Image)
- Inherited by: the site's Settings template
Step 2: Prepare the GraphQL query and test it in ide playground
query Settings($path: String!, $language: String!) {
settingItem: item(path: $path, language: $language) {
... on JSSSettings_562272ebab6e490d87f64febab0096ca{
favIcon: field(name:"favIcon")
{
jsonValue
}
}
}
}
Step 3: Understand the exitsing methods supported by SitecoreClientInit class to find appropriate option to override.
SitecoreClientInit type have a support for custom support to bring your own layout service data as you can see in below screenshot. We are going to utilize the same.
Step 4: Create fetchSettings.ts file in under lib folder
import { GraphQLRequestClient } from '@sitecore-content-sdk/core';
import { ImageField } from '@sitecore-content-sdk/nextjs';
const getSettings = `query Settings($path: String!, $language: String!) {
settingItem: item(path: $path, language: $language) {
... on JSSSettings_562272ebab6e490d87f64febab0096ca{
favIcon: field(name:"favIcon")
{
jsonValue
}
}
}
}`;
type SettingGraphQLResponse = {
settingItem: {
favIcon: {
jsonValue: ImageField;
};
};
};
const fetchSettings = async (path: string, language: string, gqlClient: GraphQLRequestClient) => {
const response = await gqlClient.request<SettingGraphQLResponse>(getSettings, {
path,
language,
});
console.log('response', response);
return response;
};
export default fetchSettings;
Step 5: Create layout.ts file in under lib folder. Use fetchLayoutData method.
// rendering_host_app/src/lib/custom-services
import { LayoutService, type LayoutServiceData } from '@sitecore-content-sdk/nextjs';
import { type GraphQLRequestClientFactory } from '@sitecore-content-sdk/nextjs/client';
import type { RouteOptions } from '@sitecore-content-sdk/core/layout';
import fetchSettings from './fetchSettings';
export class CustomLayoutService extends LayoutService {
constructor(private readonly graphQLClientFactory: GraphQLRequestClientFactory) {
super({ clientFactory: graphQLClientFactory });
}
async fetchLayoutData(routePath: string, routeOptions: RouteOptions): Promise<LayoutServiceData> {
console.log('custom layout service executed');
const layoutData = await super.fetchLayoutData(routePath, routeOptions);
if (!layoutData.sitecore.route) return layoutData;
const graphQLClient = this.graphQLClientFactory();
const contextLanguage = layoutData?.sitecore?.context?.language || routeOptions?.locale || 'en';
const siteName = layoutData?.sitecore?.context?.site?.name;
if (!siteName) return layoutData;
const contentRootPath = `/sitecore/content/SitecoreStrikersPracticeCollection/${siteName}/Settings`;
const result = await fetchSettings(contentRootPath, contextLanguage ,graphQLClient);
console.log('Fetched Settings:', JSON.stringify(result));
const faviconurl = result?.settingItem?.favIcon?.jsonValue?.value?.src;
console.log('FavIcon:', faviconurl);
return {
...layoutData,
sitecore: {
...layoutData.sitecore,
context: {
...layoutData.sitecore.context,
favIcon : faviconurl || ''
},
},
};
}
}
Step 6: Update sitecore-client.ts file in under lib folder. Add the CustomLayoutService configuration as shown below.
import { SitecoreClient } from '@sitecore-content-sdk/nextjs/client';
import scConfig from 'sitecore.config';
import { createGraphQLClientFactory } from '@sitecore-content-sdk/nextjs/client';
import { CustomLayoutService } from './custom-layout';
const clientFactory = createGraphQLClientFactory({ api: scConfig.api });
const client = new SitecoreClient({
...scConfig,
custom: {
layoutService: new CustomLayoutService(clientFactory),
},
});
export default client;
Step 7:Result.
Check the logs to see the result appended or not
Step 8:Append the favIcon on Layout.tsx to check the result on website.
import React, { JSX } from "react";
import Head from 'next/head';
import { Field, Page } from "@sitecore-content-sdk/nextjs";
import Scripts from "src/Scripts";
import SitecoreStyles from "components/content-sdk/SitecoreStyles";
import { DesignLibraryApp } from "@sitecore-content-sdk/nextjs";
import { AppPlaceholder } from "@sitecore-content-sdk/nextjs";
import componentMap from ".sitecore/component-map";
interface LayoutProps {
page: Page;
}
export interface RouteFields {
[key: string]: unknown;
Title?: Field;
}
const Layout = ({ page }: LayoutProps): JSX.Element => {
const { layout, mode } = page;
const { route } = layout.sitecore;
const mainClassPageEditing = mode.isEditing ? "editing-mode" : "prod-mode";
console.log("Layout page data:", page?.layout);
console.log("favIcon page data:", page?.layout?.sitecore?.context?.favIcon);
const rawFavIcon = page?.layout.sitecore.context?.favIcon;
const favIcon = typeof rawFavIcon === 'string' && rawFavIcon ? rawFavIcon : undefined;
return (
<>
<Scripts />
<SitecoreStyles layoutData={layout} />
<Head>
<link rel="icon" href={favIcon} />
</Head>
{/* root placeholder for the app, which we add components to using route data */}
<div className={mainClassPageEditing}>
{mode.isDesignLibrary ? (
route && (
<DesignLibraryApp
page={page}
rendering={route}
componentMap={componentMap}
loadServerImportMap={() => import(".sitecore/import-map.server")}
/>
)
) : (
<>
<header>
<div id="header">
{route && (
<AppPlaceholder
page={page}
componentMap={componentMap}
name="headless-header"
rendering={route}
/>
)}
</div>
</header>
<main>
<div id="content">
{route && (
<AppPlaceholder
page={page}
componentMap={componentMap}
name="headless-main"
rendering={route}
/>
)}
</div>
</main>
<footer>
<div id="footer">
{route && (
<AppPlaceholder
page={page}
componentMap={componentMap}
name="headless-footer"
rendering={route}
/>
)}
</div>
</footer>
</>
)}
</div>
<>
);
};
export default Layout;
Step 9:Verify result.
Thanks for reading and keep learning!!
You can check my other blogs too if interested. Blog Website




Comments
Post a Comment