Country-Specific KVs
Country-based KV overrides for ADX/GAM ads.
Country-Specific KVs
Resolve the correct waterfall KV arrays per ad placement based on the visitor's country.
Entry: lib/utils/countrySpecificKvs.js, lib/utils/adConfig.js
Goal
At page render, resolve the correct waterfall KV arrays per ad placement based on the visitor's country, using adConfig.countrySpecificKvs from the CMS API when that level's adConfig.kvMode === "country", otherwise falling back to the existing time-specific / static behavior.
The CMS stores country-bound KV sets at three levels:
| Level | Source on page | Location |
|---|---|---|
| Tenant defaults | Always available | tenant.adConfig.countrySpecificKvs + tenant.adConfig.kvMode |
| Category (URL) | Category pages | category.adConfig.countrySpecificKvs + category.adConfig.kvMode |
| Article (URL) | Article pages | article.adConfig.countrySpecificKvs + article.adConfig.kvMode |
The kvMode toggle
Each adConfig carries kvMode: "time" | "country" (absent = "time"). It selects which KV system is active at that level:
"time"→ evaluatetimeSpecificKvs(existing behavior, unchanged)."country"→ evaluatecountrySpecificKvs.- The inactive system's data may still be present in the payload — ignore it.
When every level has kvMode: "time" (or no kvMode), resolution is identical to the time-specific contract.
Visitor country detection
- Country is an ISO 3166-1 alpha-2 code, uppercase (
"US","IN","AE"). - Prefer the CDN/edge header already available to the app, e.g.
CloudFront-Viewer-Country, CloudflareCF-IPCountry, or Vercelx-vercel-ip-country. Fall back to a geo-IP lookup only if no header exists. - Normalize:
String(code).trim().toUpperCase(). - Unknown / undetectable country (missing header,
"XX","T1", etc.) → treat as no matching set → static fallback.
API payload shape
countrySpecificKvs is an optional array on adConfig. Each set:
type CountrySpecificKvSet = {
label?: string; // dashboard label, informational only
countries: string[]; // ISO alpha-2, e.g. ["US","CA"]; unique across sets per level
[kvField: string]: string[] | undefined; // per-ad-type waterfall values (two-decimal strings)
// CMS-internal fields — IGNORE on the frontend:
useHighestPrice?: boolean; // how the arrays were authored (generated vs manual)
};The frontend only reads countries and the KV arrays.
Tenant adConfig.countrySpecificKvs — KV field keys
| Key | Used on |
|---|---|
displayKv1ValuesArticle | Article pages (tenant default) |
displayKv1ValuesCategory | Category pages (tenant default) |
interstitialKv1Values | Interstitial |
rewardedKv1Values | Rewarded (first page) |
rewardedKv1ValuesSecondPage | Rewarded (second page) |
collapsibleKv1Values | Collapsible |
Category / Article adConfig.countrySpecificKvs — KV field keys
| Key | Placement |
|---|---|
displayKv1Values | Display 1 |
displayKv1Values2 | Display 2 |
interstitialKv1Values | Interstitial |
rewardedKv1Values | Rewarded (first page) |
rewardedKv1ValuesSecondPage | Rewarded (second page) |
stickyKv1Values | Sticky |
skyscrapperLeftKv1Values | Skyscraper left |
skyscrapperRightKv1Values | Skyscraper right |
collapsibleKv1Values | Collapsible |
(Same keys as time-specific.)
Resolution rules
- Detect the visitor country
CC(uppercase alpha-2, ornullif unknown). - Pick the active source level (whole-replace, mode-aware):
activeSets(adConfig) = adConfig.kvMode === "country" ? adConfig.countrySpecificKvs : adConfig.timeSpecificKvs- On category/article pages: if
activeSets(urlAdConfig)exists andlength > 0→ source = URL level (its mode, its sets). Tenant sets of both systems are ignored entirely. - Otherwise if
activeSets(tenantAdConfig)is non-empty → source = tenant level (its mode, its sets). - Otherwise → static fields only (current behavior).
- If the source level's mode is
"time"→ run the existing time-specific resolution against those sets. - If the source level's mode is
"country":- Find active set: first set whose
countriesincludesCC. - Per KV field:
- Active set exists and field is a non-empty array → use it.
- Active set exists and field missing/empty → no KV (do not fall back to static).
- No set matches
CC(orCCunknown) → use the existing static field fromadConfig(URL static → tenant static).
- Find active set: first set whose
- CMS guarantees a country appears in at most one set per level; if bad data has duplicates, first matching set wins.
label,useHighestPrice,*HighestPrice,tier*,priceCountare never used in ad logic.- AdSense pages: no change (no KV waterfalls).
Reference resolver
Extends the existing resolveTimeSpecificKvField from the time doc.
function normalizeCountry(code) {
if (!code) return null;
const value = String(code).trim().toUpperCase();
return /^[A-Z]{2}$/.test(value) ? value : null;
}
function findActiveCountrySet(sets, country) {
if (!Array.isArray(sets) || sets.length === 0 || !country) return null;
return (
sets.find(
(set) =>
Array.isArray(set?.countries) &&
set.countries.some((c) => normalizeCountry(c) === country),
) ?? null
);
}
function activeSets(adConfig) {
if (!adConfig) return null;
const sets =
adConfig.kvMode === "country"
? adConfig.countrySpecificKvs
: adConfig.timeSpecificKvs;
return Array.isArray(sets) && sets.length > 0 ? sets : null;
}
/**
* Resolve one KV field, honoring adConfig.kvMode per level.
*/
export function resolveKvField({
kvField,
urlAdConfig,
tenantAdConfig,
country,
hour,
}) {
const staticFallback = () =>
urlAdConfig?.[kvField] ?? tenantAdConfig?.[kvField];
const urlSets = activeSets(urlAdConfig);
const sourceConfig = urlSets
? urlAdConfig
: activeSets(tenantAdConfig)
? tenantAdConfig
: null;
if (!sourceConfig) return staticFallback();
if (sourceConfig.kvMode !== "country") {
return resolveTimeSpecificKvField({
kvField,
urlAdConfig,
tenantAdConfig,
hour,
});
}
const set = findActiveCountrySet(
sourceConfig.countrySpecificKvs,
normalizeCountry(country),
);
if (!set) return staticFallback();
const values = set[kvField];
if (Array.isArray(values) && values.length > 0) return values;
return undefined; // matched set, empty field → no KV, no static fallback
}Integration guide
- Replace direct calls to
resolveTimeSpecificKvFieldin the KV merge path withresolveKvField, passing the detectedcountry. - Country detection (client):
CountryProviderexposes{ country, settled }. It always starts unsettled on SSR and the first client paint (do not readdocument.cookieinuseState— that causes hydration mismatches when country-mode ads returnnullon the server and a slot on the client). After mount it settles from thevisitor_countrycookie, geo API, or timeout. - Category page:
urlAdConfig = category.adConfig; tenant static display field isdisplayKv1ValuesCategory. - Article page:
urlAdConfig = article.adConfig; tenant static display field isdisplayKv1ValuesArticle. - Rewarded second page:
kvField = "rewardedKv1ValuesSecondPage".