Skip to content

MFE Integration

Source: omni-web-content-frontend/src/repositories/sanity/

📐 ADR: ADR-0009 — Use Sanity directly for editorial content — Decision: MFEs query Sanity directly via GROQ (no BFF layer for content).

Terminal window
npm install next-sanity @sanity/client @sanity/preview-url-secret
# Required
NEXT_PUBLIC_SANITY_PROJECT_ID=<project-id>
NEXT_PUBLIC_SANITY_DATASET=production
NEXT_PUBLIC_SANITY_API_VERSION=2026-01-15
NEXT_PUBLIC_SANITY_STUDIO_URL=https://hema-cms.sanity.studio
NEXT_PUBLIC_BASE_URL=https://www.hema.nl
NEXT_PUBLIC_REQUEST_TAG_PREFIX=omni-web-content
# Server-only (for draft mode)
SANITY_API_READ_TOKEN=<read-token>
src/repositories/sanity/client.ts
import { createClient } from 'next-sanity';
export const baseClient = createClient({
projectId,
dataset,
apiVersion,
perspective: 'published',
token,
useCdn: true,
stega: { studioUrl },
requestTagPrefix: createRequestTag(requestTagPrefix),
});
SettingValueWhy
perspective'published'Only published content in production
useCdntrueCDN-cached responses for performance
stega.studioUrlStudio URLEnables click-to-edit in preview mode
requestTagPrefixService nameTags requests for monitoring in Sanity dashboard
src/repositories/sanity/preview/live.ts
import { defineLive } from 'next-sanity';
export const { sanityFetch, SanityLive } = defineLive({
client: baseClient,
serverToken: token,
browserToken: token,
});

sanityFetch automatically handles draft content when draft mode is enabled, real-time updates via Content Lake listener, and perspective switching.

Each domain has a repository class:

src/repositories/sanity/page-data-repository.ts
export default class SanityPageDataRepository implements PageRepository {
async getPageDataBySlug(slug: string, locale: string): Promise<Page | null> {
const [lang, country] = locale.split('-');
return await this.client.fetch(PAGE_QUERY, { slug, lang, country });
}
}

All GROQ queries use:

  • $lang — Language code (nl, fr, de)
  • $country — Country code lowercase (nl, be, fr, de)
  • $slug — Page slug
const [lang, country] = locale.split('-'); // "nl-nl" → ["nl", "nl"]
*[($country == '' || lower(country) == $country) &&
defined(slug[$lang].current) && slug[$lang].current == $slug][0]
{
_type == 'homePage' => { ...homePageProjection },
_type == 'flexibleContentPage' => { ...flexibleProjection },
_type == 'inspirationalBlog' => { ...blogProjection },
}
  • Country filtering: ($country == '' || lower(country) == $country)
  • Translated slug lookup: slug[$lang].current == $slug
  • Type-based projection: Different shapes per _type
selectComponent[$lang][] -> {
_type,
_type == 'carousel' => { ...carouselProjection },
_type == 'heroBlock' => { ...heroProjection },
}
export const fetchMicrocopy = async (locale: string) => {
return unstable_cache(
async () => {
const data = await baseClient.fetch(MICROCOPY_QUERY, { lang, country });
return transformEntriesToNested(data.entries, lang);
},
[`microcopy-${locale}`],
{ revalidate: 300, tags: [`microcopy-${locale}`] },
)();
};
*[_type == "translation.metadata" &&
*[slug[$lang].current == $slug][0]._id in translations[].value._ref
][0] {
"translations": translations[]{ "_key": _key, "slug": value->slug },
}
Diagram
src/app/[locale]/[...slugs]/page.tsx
export default async function Page({ params }) {
const { slugs, locale } = await params;
const path = slugs.join('/');
const pageType = (await pageService.getPageData(path, locale))?.type;
if (!pageType) notFound();
return <PageTemplateResolver pageType={pageType} path={path} locale={locale} />;
}
Data TypeCacheRevalidation
Page contentNext.js ISR300s (5 min)
Microcopyunstable_cache300s (5 min)
SitemapRoute handler3600s (1 hour)
Draft modeNo cacheReal-time
EnvironmentperspectiveuseCdnDraft Mode
ProductionpublishedtrueVia secret URL
PreviewdraftsfalseEnabled
DevelopmentpublishedtrueAvailable locally