The platform filters sections, pages, and navigation by the context of the request. The theme engine
gives that context to pageDataResolver. The theme must forward the context to the theme APIs.
Use this page when server-side rendering and the hydrated client disagree about the device, the login state, the user groups, the customer status, the experiments, or the location.
Context contract
| Context | Resolver source | Outbound header or action | Purpose |
|---|---|---|---|
| User agent | headers.userAgent | user-agent | Device detection |
| Platform user data | headers.userData | x-user-data-minified | Login-state filters |
| External user data | headers.userData or an external service | x-external-user-data | Login-state filters for a headless setup |
| User groups | cookies.userGroups | user_groups and user-groups | Group filters |
| User status | headers.userStatus or an external service | x-user-status | New-customer and existing-customer filters |
| Experimental features | headers.experimentalFeatures or an external service | x-experimental-features | Experiment filters |
| Location | cookies.userAppLocationDetails | Logistics location call | Theme-zone filters |
User agent
The theme engine sends the user agent as headers.userAgent. Forward the value to the page call.
export async function pageDataResolver(pageResolverData) {
const { fpi, themeId, headers } = pageResolverData;
const { userAgent } = headers;
const requestHeaders = {};
if (userAgent) requestHeaders["user-agent"] = userAgent;
if (pageValue && pageValue !== currentPageInStore) {
APIs.push(
fpi.theme.getPage({
pageValue,
themeId,
requestHeaders,
})
);
}
return Promise.all(APIs).catch(console.log);
}
Platform user data
The theme engine sends the platform user data as headers.userData. The platform reads only whether
the value is present. The platform does not read the profile of the user.
Forward the smallest value that the platform accepts. Do not put a user profile into the state during server-side rendering.
export async function pageDataResolver(pageResolverData) {
const { fpi, themeId, headers } = pageResolverData;
const { userData } = headers;
const requestHeaders = {};
if (userData) {
requestHeaders["x-user-data-minified"] =
typeof userData === "object" ? JSON.stringify(userData) : userData;
}
if (pageValue && pageValue !== currentPageInStore) {
APIs.push(fpi.theme.getPage({ pageValue, themeId, requestHeaders }));
APIs.push(fpi.content.getNavigations(requestHeaders));
}
return Promise.all(APIs).catch(console.log);
}
External user data
Use x-external-user-data when a system outside the platform owns authentication. Any truthy JSON
value marks the user as logged in.
The upstream system sends the value
Read headers.userData during server-side rendering. Store the value in the custom state, because
later client-side requests do not have the header.
export async function pageDataResolver(pageResolverData) {
const { fpi, themeId, headers } = pageResolverData;
const state = fpi.store.getState();
const requestHeaders = {};
const { userData } = headers;
if (userData) {
fpi.custom.setValue("userData", userData);
requestHeaders["x-external-user-data"] =
typeof userData === "object" ? JSON.stringify(userData) : userData;
} else {
const userDataInState = state?.custom?.userData ?? "";
if (userDataInState) {
requestHeaders["x-external-user-data"] =
typeof userDataInState === "object"
? JSON.stringify(userDataInState)
: userDataInState;
}
}
if (pageValue && pageValue !== currentPageInStore) {
APIs.push(fpi.theme.getPage({ pageValue, themeId, requestHeaders }));
}
return Promise.all(APIs).catch(console.log);
}
The theme requests the value
export async function pageDataResolver(pageResolverData) {
const { fpi, themeId } = pageResolverData;
const requestHeaders = {};
const userDetails = await callUserDetailsAPI(fpi);
if (userDetails) {
requestHeaders["x-external-user-data"] =
typeof userDetails === "object" ? JSON.stringify(userDetails) : userDetails;
}
if (pageValue && pageValue !== currentPageInStore) {
APIs.push(fpi.theme.getPage({ pageValue, themeId, requestHeaders }));
}
return Promise.all(APIs).catch(console.log);
}
The server does not know the login state before the first render. The content is one request behind.
User groups
The theme engine sends the user groups as cookies.userGroups. Send both header spellings.
Fetch the user groups on the client only. Read Coding Practices for the reason.
export async function pageDataResolver(pageResolverData) {
const { fpi, themeId, cookies } = pageResolverData;
await fetchUserGroups(fpi);
const { userGroups } = cookies;
const requestHeaders = {};
if (userGroups) {
requestHeaders["user_groups"] = userGroups;
requestHeaders["user-groups"] = userGroups;
}
if (pageValue && pageValue !== currentPageInStore) {
APIs.push(fpi.theme.getPage({ pageValue, themeId, requestHeaders }));
}
return Promise.all(APIs).catch(console.log);
}
User status
The x-user-status header accepts two values only:
new_userexisting_user
An external system derives the value. The platform applies the filter.
export async function pageDataResolver(pageResolverData) {
const { fpi, themeId, headers } = pageResolverData;
const state = fpi.store.getState();
const requestHeaders = {};
const { userStatus } = headers;
if (userStatus) {
fpi.custom.setValue("userStatus", userStatus);
requestHeaders["x-user-status"] = userStatus;
} else {
const userStatusInState = state?.custom?.userStatus ?? "";
if (userStatusInState) requestHeaders["x-user-status"] = userStatusInState;
}
if (pageValue && pageValue !== currentPageInStore) {
APIs.push(fpi.theme.getPage({ pageValue, themeId, requestHeaders }));
}
return Promise.all(APIs).catch(console.log);
}
If the theme requests the status from another service instead, the content is one request behind.
Experimental features
The x-experimental-features header accepts an array of strings or a comma-separated string. An
external system derives the values. The platform applies the filter.
export async function pageDataResolver(pageResolverData) {
const { fpi, themeId, headers } = pageResolverData;
const state = fpi.store.getState();
const requestHeaders = {};
const { experimentalFeatures } = headers;
if (experimentalFeatures) {
fpi.custom.setValue("experimentalFeatures", experimentalFeatures);
requestHeaders["x-experimental-features"] = experimentalFeatures;
} else {
const featuresInState = state?.custom?.experimentalFeatures ?? "";
if (featuresInState) {
requestHeaders["x-experimental-features"] = featuresInState;
}
}
if (pageValue && pageValue !== currentPageInStore) {
APIs.push(fpi.theme.getPage({ pageValue, themeId, requestHeaders }));
}
return Promise.all(APIs).catch(console.log);
}
Location details
A theme-zone filter needs a pin code. The theme engine sends the location as
cookies.userAppLocationDetails. The source cookie is app_location_details.
Show a pin-code selector on the landing page when the business uses theme zones.
async function fetchLocationDetailsOnServer(fpi, cookies) {
if (isRunningOnClient()) return;
let pincode;
const { userAppLocationDetails: locationDetails } = cookies;
if (locationDetails) {
try {
if (typeof locationDetails === "string") {
const parsedLocation = JSON.parse(locationDetails);
if (parsedLocation && parsedLocation.pincode) {
pincode = parsedLocation.pincode;
}
} else if (locationDetails.pincode) {
pincode = locationDetails.pincode;
}
} catch {
console.error("Failed to parse locationDetails from cookies:", locationDetails);
}
}
if (pincode) {
await fpi.logistic.getPincodeCity({ pincode });
}
}
Call the helper before the page request:
export async function pageDataResolver(pageResolverData) {
const { fpi, themeId, cookies } = pageResolverData;
await fetchLocationDetailsOnServer(fpi, cookies);
const applicationData = fpi.getters.APPLICATION(fpi.store.getState());
const company = applicationData?.company_id;
if (pageValue && pageValue !== currentPageInStore) {
APIs.push(
fpi.theme.getPage({ pageValue, themeId, requestHeaders, company })
);
}
return Promise.all(APIs).catch(console.log);
}
To use a fallback pin code, set the default before you read the cookie:
let pincode = "400069";
Headless storefronts
In a headless setup an external system owns authentication and user data, and the platform supplies only content. The platform still filters sections, pages, and navigation, but it cannot read a platform session, so the theme must supply every filter input.
Which header to send
| Filter dimension | Header | Notes |
|---|---|---|
| Login state | x-external-user-data | Any truthy JSON marks the visitor as logged in |
| User segments | user_groups and user-groups | Send both spellings |
| Platform | user-agent | Forward the incoming value |
| New or existing customer | x-user-status | new_user or existing_user only |
| Experiments | x-experimental-features | Array of strings, or a comma-separated string |
| Zone | Logistics location call | Prime the pin code, do not send a header |
Do not send x-user-data-minified and x-external-user-data together. Choose the authentication
model the storefront uses. Sending both makes the login state ambiguous.
Send the smallest value
The platform reads only whether the value is present. It does not read the profile of the visitor. Send the minimum that proves a session exists, never a full customer record. Anything you send during server-side rendering is written into the cached HTML. Read Coding Practices for why that matters.
Context that arrives one request late
When the theme fetches the context itself rather than receiving it on the request, the server does not have it for the first render. That first page is filtered as though the visitor were anonymous.
Two ways to handle it:
- Have the upstream system send the value as a request header. The first render is then correct.
- Accept the delay, and refetch the page after the value arrives when the difference is visible.
Persist the value with fpi.custom.setValue so later client-side navigation keeps it. The browser
call does not receive headers.userStatus, headers.userGroups, or headers.experimentalFeatures.
Verification
Check each filter you enable with a positive and a negative case. Then navigate client-side and confirm the context still applies. Check edit mode separately: it must show content without targeting filters.
If targeting misbehaves, inspect the request the theme sends to the page and navigation calls before you change any section code.
Rules
- Build one
requestHeadersobject for each run of the resolver. - Forward only the context that the request contains.
- Store context in the custom state only when a client-side request needs it.
- Never put private user data into server-rendered state.
- Send the same context to the navigation call when a navigation filter uses it.
- Confirm whether your FDK Store version exposes
getPageorfetchPage.