Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
6a1aab6
Add Braze banner integration to Feast contextual nudge component
andresilva-guardian Jun 17, 2026
79f34e5
Add nudgeIndex and idApiUrl to FeastContextualNudge story args
andresilva-guardian Jun 17, 2026
eda9dd0
Merge branch 'main' into afs/feast-contextual-nudge-braze-banners
andresilva-guardian Jun 24, 2026
2bcaf2f
Add GetContext message type and context parameter to Braze Banners Sy…
andresilva-guardian Jun 24, 2026
1a2962c
Add context prop to FeastContextualNudge component
andresilva-guardian Jun 24, 2026
0c4ea5e
Remove unused text color variables from FeastContextualNudge component
andresilva-guardian Jun 24, 2026
d82581c
Refactor FeastContextualNudge component to replace subtext variables …
andresilva-guardian Jun 24, 2026
d18edc4
Wrap BrazeBannersSystemDisplay in a div with aria-description for acc…
andresilva-guardian Jun 24, 2026
e6f0950
Add placement management for Braze Banners System and update refresh …
andresilva-guardian Jun 25, 2026
4802c0e
Enhance logging for Braze banners updates to provide clearer information
andresilva-guardian Jun 26, 2026
bcc02f4
Add stale placement handling for Braze banners to improve fallback logic
andresilva-guardian Jun 26, 2026
25b88c1
Refactor Braze Banners System to implement per-placement stale suppre…
andresilva-guardian Jun 26, 2026
212591f
Refactor getAdjustToken function for cleaner token retrieval in Feast…
andresilva-guardian Jun 30, 2026
5f83369
Merge branch 'main' into afs/feast-contextual-nudge-braze-banners
andresilva-guardian Jul 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 75 additions & 12 deletions dotcom-rendering/src/components/FeastContextualNudge.island.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ import {
} from '@guardian/source/foundations';
import { LinkButton } from '@guardian/source/react-components';
import { useEffect, useState } from 'react';
import {
BrazeBannersSystemDisplay,
BrazeBannersSystemPlacementId,
isPlacementStale,
} from '../lib/braze/BrazeBannersSystem';
import { useAB } from '../lib/useAB';
import { useBraze } from '../lib/useBraze';
import type { StageType } from '../types/config';
import type { RecipeBlockElement } from '../types/content';
import { useConfig } from './ConfigContext';
Expand All @@ -16,10 +22,8 @@ import { useConfig } from './ConfigContext';

const FEAST_BG = '#F3F3E9';
const FEAST_BG_DARK = '#2B2B26';
const FEAST_TEXT = sourcePalette.neutral[10];
const FEAST_TEXT_DARK = sourcePalette.neutral[100];
const FEAST_SUBTEXT = sourcePalette.neutral[20];
const FEAST_SUBTEXT_DARK = sourcePalette.neutral[93];
const FEAST_TEXT = sourcePalette.neutral[20];
const FEAST_TEXT_DARK = sourcePalette.neutral[93];
const FEAST_GREEN = '#68773C';
const FEAST_GREEN_HOVER = '#4d5c2b';
const FEAST_BORDER = FEAST_GREEN;
Expand All @@ -29,15 +33,13 @@ const FEAST_BORDER_DARK = FEAST_GREEN;

const lightVars = css`
--feast-nudge-bg: ${FEAST_BG};
--feast-nudge-heading: ${FEAST_TEXT};
--feast-nudge-subtext: ${FEAST_SUBTEXT};
--feast-nudge-text: ${FEAST_TEXT};
--feast-nudge-border: ${FEAST_BORDER};
`;

const darkVars = css`
--feast-nudge-bg: ${FEAST_BG_DARK};
--feast-nudge-heading: ${FEAST_TEXT_DARK};
--feast-nudge-subtext: ${FEAST_SUBTEXT_DARK};
--feast-nudge-text: ${FEAST_TEXT_DARK};
--feast-nudge-border: ${FEAST_BORDER_DARK};
`;

Expand All @@ -46,9 +48,12 @@ const darkVars = css`
const FEAST_ADJUST_TOKEN_PROD = '20wmhy68';
const FEAST_ADJUST_TOKEN_CODE = '20o7ykck';

const getAdjustToken = (stage: StageType): string => {
return stage === 'PROD' ? FEAST_ADJUST_TOKEN_PROD : FEAST_ADJUST_TOKEN_CODE;
};

const buildFeastLink = (recipeId: string, stage: StageType): string => {
const token =
stage === 'PROD' ? FEAST_ADJUST_TOKEN_PROD : FEAST_ADJUST_TOKEN_CODE;
const token = getAdjustToken(stage);
return `https://guardian-feast.go.link/recipe/${encodeURIComponent(
recipeId,
)}?adj_t=${encodeURIComponent(token)}`;
Expand Down Expand Up @@ -109,7 +114,7 @@ const buttonWrapperStyles = css`

const descriptionStyles = css`
${article15};
color: var(--feast-nudge-subtext);
color: var(--feast-nudge-text);
b {
font-weight: bold;
}
Expand All @@ -122,6 +127,8 @@ type FeastContextualNudgeProps = {
recipeArticleTitle: string;
pageId: string;
isDev: boolean;
nudgeIndex: number;
idApiUrl: string | undefined;
};

/**
Expand All @@ -142,13 +149,17 @@ export const FeastContextualNudge = ({
recipeArticleTitle,
pageId,
isDev,
nudgeIndex,
idApiUrl,
}: FeastContextualNudgeProps) => {
const abTests = useAB();
const isVariant =
abTests?.isUserInTestGroup('feast-recipe-nudge-v2', 'variant-1') ??
false;

const { darkModeAvailable } = useConfig();
const { darkModeAvailable, renderingTarget } = useConfig();

const { braze } = useBraze(idApiUrl ?? '', renderingTarget);

const [isStorybook, setIsStorybook] = useState(false);
useEffect(() => {
Expand All @@ -174,6 +185,58 @@ export const FeastContextualNudge = ({

if (!isVariant) return null;

// If idApiUrl is defined and Braze has a banner for this placement slot,
// render the Braze banner instead of the native nudge.
if (idApiUrl !== undefined) {
const placementId =
BrazeBannersSystemPlacementId[
`FeastContextualNudge${nudgeIndex}` as keyof typeof BrazeBannersSystemPlacementId
];

// Guard against stale placements: if the last requestBannersRefresh
// was rate-limited AND this placement has suppressOnStale: true in
// PLACEMENT_SUPPRESS_ON_STALE, skip getBanner() and fall through to
// the native nudge below.
//
// Each FeastContextualNudge placement ID has its own entry in
// PLACEMENT_SUPPRESS_ON_STALE — change any individual one to `true`
// to suppress that specific nudge on a failed refresh.
const banner = !isPlacementStale(placementId)
? (braze?.getBanner(placementId) ?? null)
: null;

if (banner && braze) {
return (
<div
aria-description={`Open the recipe ${title} in the Feast app`}
data-component="feast-contextual-nudge"
css={css`
margin: ${space[2]}px 0;
`}
>
<BrazeBannersSystemDisplay
meta={{
id: `feast-contextual-nudge-${nudgeIndex}`,
braze,
banner,
}}
idApiUrl={idApiUrl}
stage={stage}
context={{
recipe,
recipeArticleTitle,
pageId,
isDev,
nudgeIndex,
darkMode: darkModeAvailable,
adjustToken: getAdjustToken(stage),
}}
/>
</div>
);
}
}

return (
<div
aria-description={`Open the recipe ${title} in the Feast app`}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ const meta = {
recipeArticleTitle: "Meera Sodha's spring onion pancakes",
recipe: mockRecipe,
isDev: true,
nudgeIndex: 1,
idApiUrl: undefined,
},
parameters: {
chromatic: { viewports: [375, 740, 980] },
Expand Down
18 changes: 17 additions & 1 deletion dotcom-rendering/src/lib/ArticleRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,11 +158,25 @@ export const ArticleRenderer = ({

const result: (JSX.Element | null | undefined)[] = [...preSection];

/**
* Distribute up to 5 Braze placement slots evenly across all sections.
* interval = ceil(sections.length / 5)
* A section at 0-based index i gets nudgeIndex = (i+1)/interval
* only when (i+1) is an exact multiple of interval (and ≤ 5).
* All other sections get nudgeIndex = null (no nudge rendered).
*/
const MAX_NUDGES = 5;
const interval = Math.ceil(sections.length / MAX_NUDGES);

for (const section of sections) {
const position = section.index + 1; // 1-based
const nudgeIndex =
position % interval === 0 ? position / interval : null;

result.push(
<Fragment key={`recipe-section-${section.index}`}>
{section.subheadingEl}
{section.recipe && (
{section.recipe && nudgeIndex !== null && (
<Island
priority="feature"
defer={{ until: 'visible' }}
Expand All @@ -173,6 +187,8 @@ export const ArticleRenderer = ({
pageId={pageId}
recipe={section.recipe}
recipeArticleTitle={section.recipeArticleTitle}
nudgeIndex={nudgeIndex}
idApiUrl={idApiUrl}
/>
</Island>
)}
Expand Down
Loading
Loading