N8+US ← all work LinkedIn Résumé (PDF)

2023–2026 · BuzzFeed · Lead author

Callout — on-site promotions admin platform

Built from scratch in Next.js: non-engineers compose audience-targeted promotions with a live preview driven by the same production renderer that serves readers.

Next.jsReactJavaScriptlive previewundo/redoautosaveaudience targeting

What I built

I built the Callout admin app from scratch in Next.js — the operator surface of a three-tier system (a reader-facing renderer, a selection API, and this admin), where I was sole author of the admin tier. Non-engineers use it to compose and target on-site promotions without a code change.

  • Campaign listing + multi-step create/edit wizard with undo/redo and autosave.
  • Live WYSIWYG preview that renders through the same component the production site uses — what you see is what ships.
  • Image upload & cropping, and an audience / device / trigger condition builder.

How it works

A campaign has an action (newsletter signup or link click), the page types it runs on, and layered typed conditions — device, referral source, logged-in, max-views-per-user, already-converted, newsletter status, section, tags, quiz, shopping class — plus triggers (time delay, exit intent, quiz-complete, engagement, game-level).

  • Selection: reject anyone who's seen a promo in the last 48h (a global frequency cap) → take active campaigns → filter by every condition → weight-based pick → shuffle to a variant that actually has content.
  • Live preview shares the literal production renderer component, and a reader-facing preview link can force a specific campaign onto any live page.

Constraint is a feature. As the tool matured we deliberately walked free-form options back into governed presets: a theme dropdown replaced open-ended background color, body copy switched automatically between white and black based on the background (with accessible contrast computed as CSS variables in the renderer), and spacing and type rules came from the design file as source of truth. The button stayed the one fully editable element — a conscious call about where operators could safely have freedom, and the same judgment a token system encodes: constrain by default, allow freedom only where breakage is acceptable.

How I built it

Frontend (my build). The admin app — listing, multi-step wizard, and live preview — in Next.js 14 / React 18: styled-components, react-hook-form + zod for schema-validated forms, `use-undo` for undo/redo, custom localStorage autosave, and react-image-crop; tested with Jest + Testing Library (plus a separate integration config) and react-axe accessibility auditing.

The shared render unit. I also built and maintained the Callout render unit — the `CalloutUnitRenderer` in BuzzFeed's shared React component library, a versioned internal npm package. The same component renders the live preview in the admin and the promotion a reader actually sees, so "what you see is what ships" is literal, not aspirational. In it I built the popup units (newsletter-signup, link-click), the trigger system (a state-machine `useTrigger` handling exit-intent, quiz-complete, and game-level triggers, with pause/resume and race-condition handling), the background-pattern / texture treatments, and device-aware rendering.

Integrated with and extended the backend. The selection API was owned by another team; I integrated the admin against it and contributed targeted API changes — device-type targeting end to end (API values, admin UI, and the render payload), game-level-complete trigger support, tighter quiz-trigger validation, a new "Games" target-page section, and extending `GET /campaigns` to return each campaign's page and targeting conditions. I did not author the selection service itself.

How the work was done: spec-first and AI-agent-scaffolded — every non-trivial change landed a checked-in feature spec with a file-by-file plan and acceptance criteria.

The contract came first. I designed the JSON data structure the whole system speaks — shaped so the admin form could set values under complex rules, validation held end to end, WYSIWYG updates pushed over browser events, and the same data rendered live on site. The renderer's first implementation pass came from a teammate building against that contract; it held across the team boundary, which is the real test of an interface. I took over the render unit's development later. Along the way I worked directly with our designer on the theme system and with the newsletter org — the driving stakeholder for themed callouts — with our PM coordinating the feature work.

Questions this project answers

What's the hardest part of building a tool non-engineers use every day? Making powerful things feel safe. The campaign wizard exposes layered targeting, triggers, and variants, so the work was hiding that complexity behind undo/redo, autosave, and a live preview that shows the real result before anything ships. The bar isn't "an engineer could operate it" — it's that a marketer can compose a targeted promotion, see exactly what a reader will see, and never need a code change.

*Why does it matter that the editor previews through the same renderer as production?* Because a preview that only approximates the real thing quietly erodes trust — operators stop believing it and start shipping to check. I built the admin's live preview on the literal production render unit, so "what you see is what ships" is a guarantee, not a slogan. It also collapses two code paths into one: there's no separate preview renderer to drift out of sync.

What does it mean to own a piece of a shared design-system library, not just an app? The render unit lives in a versioned internal component library that other products depend on, so a change there is a change to everyone's contract. That raised the bar: the trigger system became a proper state machine with pause/resume and race handling, the popup units were built to be reused, and everything shipped with tests because a regression wouldn't just break my app. Owning a shared component is as much about API stability and blast radius as it is about the feature.

How do you represent work where you extended a backend another team owned? Honestly and specifically. I was sole author of the admin tier and the render unit; the selection service belonged to another team, so I integrated against it and contributed targeted API changes — device-type targeting end to end, new trigger types, a new target-page section, and extending an endpoint to return the targeting data the admin needed. I'd rather say exactly what I built and what I extended than blur the line to sound like I owned more.

Results

  • What you see is what ships — the editor previews through the same renderer that serves readers.
  • A 48-hour global frequency cap protects readers from promo fatigue.
  • Launched with no negative impact on core site metrics (time-on-site, PVs/session); actively maintained 2023–2026.

Skills

AI / Agentic

Spec-first feature workflowEvery non-trivial change landed a checked-in spec with a file-by-file plan and acceptance criteria.
AI-agent-scaffolded developmentBuilt the admin with an agent-assisted, spec-driven loop.

Frontend

Next.js · ReactBuilt the Callout admin app from scratch — listing, wizard, preview.
Shared render unit (internal npm package)Built + maintained the CalloutUnitRenderer in BuzzFeed's shared React component library — the same component powers the admin preview and the live promotion.
Trigger system (state machine)Built the useTrigger state machine: exit-intent, quiz-complete, and game-level triggers with pause/resume and race-condition handling.
styled-componentsStyled the admin surfaces and the render unit's popup + background-pattern treatments.
react-hook-form + zodSchema-validated the multi-step campaign create/edit forms.
undo/redo + autosaveWired use-undo and localStorage autosave into the wizard.
Live WYSIWYG previewRendered the preview through the shared render unit that also serves readers.
Jest + Testing Library · react-axeUnit/integration tests and accessibility auditing across the admin and the render unit.

Backend / Infra

API integrationIntegrated the admin against the team-owned FastAPI selection service (auth, REST, caching behavior).
Targeted API changes — device targetingAdded device-type targeting end to end (API values, admin UI, and the render payload).
Trigger + targeting endpointsAdded game-level-complete triggers, tightened quiz-trigger validation, and a new 'Games' target-page section.
GET /campaigns extensionExtended the campaigns endpoint to return each campaign's page and targeting conditions the admin needed.

Leadership

Owned the admin tierSole author of the operator-facing tier of a three-tier system.
Cross-team deliveryCoordinated backend API changes with the owning team.
Shipped to productionLaunched with no negative impact on core site metrics.