Skip to content

feat: add Carbon Ads Top Bar and Sidebar placements - #9250

Open
mcollina wants to merge 4 commits into
mainfrom
feat/carbon-ads
Open

mcollina wants to merge 4 commits into
mainfrom
feat/carbon-ads

Conversation

@mcollina

@mcollina mcollina commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Description

Adds Carbon Ads, served through the OpenJS Foundation's Carbon project for Node.js, using two formats:

  • Top Bar (CWBI6KJY): full width, below the navigation, on the about, article, blog-post, blog-category, download and download-archive layouts.
  • Sidebar (CWBI6KJU): below the table of contents. It appears on about/article from xl (1280px) and on blog-post from ml (896px). Below those widths it is neither shown nor requested, as the format requires.

The homepage and the error/404 pages never render ad markup, reserve no space and make no ad requests. Layouts opt in explicitly (<WithNavBar ads />, <WithMetaBar ads="article" | "post" />), and ads are off by default. The fallback page layout has no ads.

Brian Edit Start

image

with Banner

image

Brian Edit End

Ads are rendered from Carbon's JSON API directly in the browser, without third-party scripts:

  • apps/site/util/carbonAds.ts: requests ads, validates the untrusted response (required fields, hex colours, HTTPS URLs; anything invalid means no ad), and measures viewability. The view beacon fires once, after at least 50% of the ad has been visible for one continuous second while the tab is visible.
  • apps/site/providers/carbonAdsProvider.tsx: tracks page views by locale and pathname. Each format is requested at most once per page view, so StrictMode, remounts, rerenders, theme changes and hash links never record extra impressions, and a revisited page gets a fresh ad.
  • apps/site/components/CarbonAds: renders both formats. While loading, the slot reserves space; when there is no ad (an empty response, an error or an ad blocker), the slot collapses.
  • MetaBar (ui-components) gains a generic optional footer slot, opt-in and with no ad-specific code.

Staging vs production

Every request includes ignore=yes, so impressions are not counted, unless the build sets NEXT_PUBLIC_CARBON_ADS_PRODUCTION=true. Local development and preview deployments are therefore non-counting by default. The variable is embedded at build time and must only be set for the production deployment. See docs/carbon-ads.md for the configuration and Carbon's launch sequence:

  1. Carbon reviews a staging URL.
  2. Production is enabled.
  3. For seven days Carbon counts traffic but returns empty responses, so the slots render nothing.
  4. Paid ads then start appearing in the same slots.

Learn and the API docs are separate applications and are not affected.

Validation

  • pnpm lint, lint:types (site and ui-components), Prettier and pnpm build pass.
  • Unit tests: site 247/247 and ui-components 45/45 pass. New coverage:
    • validation of the response data
    • fetch failure modes
    • viewability timing: 49%, 999ms, threshold resets, hidden tab, cleanup
    • StrictMode, remounts, A→B→A and locale changes
    • stale responses after navigation
    • Sidebar eligibility
    • the MetaBar footer slot
  • A new e2e spec, tests/e2e/carbon-ads.spec.ts, uses mocked Carbon responses to check:
    • no requests and no markup on the homepage
    • one request per format per page view across client-side navigation
    • no Sidebar request on narrow screens
  • A manual Playwright check against next start, with fixture responses using maximum-length creatives, passed all 238 checks:
    • every layout at 375, 768, 895/897, 1279/1281 and 1600px
    • localized homepages, loaded directly and after client-side navigation
    • light and dark themes
    • empty and blocked responses
    • a short desktop viewport
    • resizing across the breakpoints
    • keyboard focus and horizontal overflow

Note: the e2e spec caught a React Compiler issue that unit tests (which run without the compiler) could not catch. The compiler hoisted the page-view Map/Set out of the useMemo, which shared them across navigations. The objects are now created through a function of the page-view key.

Reviewers might want to check:

  • The Sidebar styling, including the gradient fading into the image, against Carbon's reference CSS.
  • The 96px Top Bar reservation on narrow screens. It fits typical copy, but a maximum-length description is about 137–157px tall and causes a small layout shift.

Related Issues

None.

Check List

  • I have read the Contributing Guidelines and made commit messages that follow the guideline.
  • I have run pnpm format to ensure the code follows the style guide.
  • I have run pnpm test to check if all tests are passing.
  • I have run pnpm build to check if the website builds without errors.
  • I've covered new added functionality with unit tests if necessary.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Sfj4nGFRyF6F4tLo3KmVjH

@mcollina
mcollina requested review from a team as code owners October 30, 2026 11:21
@vercel

vercel Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
nodejs-org Ready Ready Preview Nov 3, 2026 1:34am UTC

Request Review

@codecov

codecov Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.61538% with 16 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.01%. Comparing base (53d777b) to head (da292f1).
⚠️ Report is 44 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
apps/site/hooks/useCarbonAd.ts 84.09% 13 Missing and 1 partial ⚠️
...ges/ui-components/src/Containers/MetaBar/index.tsx 60.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9250      +/-   ##
==========================================
+ Coverage   86.14%   87.01%   +0.86%     
==========================================
  Files          87       89       +2     
  Lines        6164     6459     +295     
  Branches      369      391      +22     
==========================================
+ Hits         5310     5620     +310     
+ Misses        850      834      -16     
- Partials        4        5       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@ovflowd

ovflowd commented Oct 30, 2026

Copy link
Copy Markdown
Member

Oh boi... here we go.

@mcollina could you add screenshots of how it looks when there are ads?

  • do we already have plans to deal with the potential negative PR and influx of issues that will come with the fact we are serving ads? 🙈

@mcollina

Copy link
Copy Markdown
Member Author

These are the same ads mdn uses. I'll leave @rginn handle the public comms

@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

📦 Build Size Comparison

Summary

Metric Value
Old Total First Load JS 7.21 MB
New Total First Load JS 7.21 MB
Delta -2.23 KB (-0.03%)

Changes

🔄 Modified Routes (5)
Route Old First Load JS New First Load JS Delta
/[locale] 1.69 MB 1.69 MB 📉 -1.26 KB (-0.07%)
/[locale]/[...path] 1.69 MB 1.69 MB 📉 -1.26 KB (-0.07%)
/[locale]/blog/[...path] 1.69 MB 1.69 MB 📉 -1.26 KB (-0.07%)
/[locale]/download/archive/[version] 1.69 MB 1.69 MB 📉 -1.26 KB (-0.07%)
/_not-found 444.36 KB 447.16 KB 📈 2.80 KB (+0.63%)

@ovflowd

This comment was marked as outdated.

@bmuenzenmeyer

This comment was marked as outdated.

@bmuenzenmeyer

bmuenzenmeyer commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

personally, I'm opposed to:

  • we also use banners in that region to signify major events like security releases. we are now intentionally drawing eyeballs up there for multiple, competing needs.
  • the top bar interrupting the nav to content eyeline and using extremely contrasting color.
  • the logo on the rightleft looks like too website headers loaded.

Important

Per content vs code, this is only my opinon.

@avivkeller

This comment was marked as outdated.

@bmuenzenmeyer

This comment was marked as outdated.

@bmuenzenmeyer

Copy link
Copy Markdown
Contributor

Learn and the API docs are separate applications and are not affected.

We've taken aims to make the three deployments look like a singular site, so I imagine we will have to extend this to doc-kit for at least learn, and perhaps API docs too.

doc-kit consumers from other projects may want this functionality anyway

@MattIPv4

Copy link
Copy Markdown
Member

👀 Two thoughts:

  1. The downloads page is what I'd consider to be quite an important/sensitive page. Are we worried about this rendering above the actual download information / pushing that information down?

  2. Are we concerned about the visuals of having a banner ad render below the nav while also having our own announcement banner render above the nav?

@ovflowd

ovflowd commented Oct 30, 2026

Copy link
Copy Markdown
Member

Learn and the API docs are separate applications and are not affected.

We've taken aims to make the three deployments look like a singular site, so I imagine we will have to extend this to doc-kit for at least learn, and perhaps API docs too.

doc-kit consumers from other projects may want this functionality anyway

Right, I'm talking to @avivkeller in person and we should make a @node-core/analytics package to put all analytics related things in there... Or maybe a component inside @node-core/ui-components like a Provider and a Component for Carbon and then the client/API keys etc get injected on each App.

@ovflowd ovflowd left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a formal block for now due to the technical/design open questions we need to figure out first.

To be clear, we are not against the addition and we will comply to it, but we oughta get this aligned as anything else here first :)

Thank you for submitting this, tho, @mcollina

@mcollina

Copy link
Copy Markdown
Member Author

Can someone with better design skills take over on where to place those?
I literally throw this over the fence, and I'm not exactly the best person qualified for the job.

@bmuenzenmeyer @ovflowd wdyt about proposing something?

@mcollina

Copy link
Copy Markdown
Member Author

👀 Two thoughts:

  1. The downloads page is what I'd consider to be quite an important/sensitive page. Are we worried about this rendering above the actual download information / pushing that information down?

Yes, I think we can skip for this

  1. Are we concerned about the visuals of having a banner ad render below the nav while also having our own announcement banner render above the nav?

I'm not the best person to lead a long collaborative UI design discussion, and it would be great if somebody else take over.

@ovflowd

ovflowd commented Oct 30, 2026

Copy link
Copy Markdown
Member

Can someone with better design skills take over on where to place those? I literally throw this over the fence, and I'm not exactly the best person qualified for the job.

@bmuenzenmeyer @ovflowd wdyt about proposing something?

Absolutely, don't worry, we'll take care of it :)

@bmuenzenmeyer

Copy link
Copy Markdown
Contributor
  1. The downloads page is what I'd consider to be quite an important/sensitive page. Are we worried about this rendering above the actual download information / pushing that information down?

Yes, I think we can skip for this

addressed via 7b4b748

@bmuenzenmeyer

Copy link
Copy Markdown
Contributor

myself, @ovflowd and @avivkeller did an impromptu session

image

with potential security releases

image

as opposed to a potential future where we get this

image

cc @mcollina

@MattIPv4

MattIPv4 commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

with potential security releases
image

Very much a fan of this 👍

@bmuenzenmeyer

Copy link
Copy Markdown
Contributor

Pushed the new topbar strategy to all in-scope layouts for further review.
Aligned the border radii with other banners of this type
Updated the docs

Comment thread apps/site/hooks/useCarbonAd.ts Resolved
Comment thread apps/site/hooks/useCarbonAd.ts Resolved
Comment thread apps/site/hooks/useCarbonAd.ts Resolved

@ovflowd ovflowd left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM with two final comments

@avivkeller avivkeller left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking to prevent accidental merges – @mcollina @rginn when should we actually merge this change and begin ads on the website?

Co-authored-by: Claudio Wunder <cwunder@gnome.org>
Co-authored-by: Brian Muenzenmeyer <brian.muenzenmeyer@gmail.com>
Co-authored-by: Matteo Collina <hello@matteocollina.com>
@bmuenzenmeyer

Copy link
Copy Markdown
Contributor

@avivkeller what happened to the docs? did yours or mine force push remove it?

@bmuenzenmeyer bmuenzenmeyer left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Portions of the original were lost on subsequent force pushes. Tests, docs, etc.

edit: i'm using an agent and the reflog to recover some of them

Comment thread apps/site/hooks/useCarbonAd.ts Dismissed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved ad validation, tracking, and placement defects can cause rendering crashes, duplicate impressions, hidden-tab beacons, missing required ads, and layout shifts.

6 open issues
What changed in this PR

Adds Carbon Ads to the website through browser-side JSON requests, with Top Bar and Sidebar components and a reusable footer slot in the shared MetaBar.

Changes:

  • Adds Top Bar placements to about, article and blog layouts, plus Sidebar ads through MetaBar.
  • Introduces ad fetching, view-beacon timing, responsive styling and a build-time production-counting flag.
  • Adds unit and end-to-end coverage, deployment documentation and a UI-components changeset.
File Description
packages/​ui-components/​src/​Containers/​MetaBar/​index.tsx Adds an optional footer slot.
packages/​ui-components/​src/​Containers/​MetaBar/​__tests__/​index.test.jsx Tests footer rendering and default behavior.
docs/​carbon-ads.md Documents placements, configuration and launch steps.
apps/​site/​turbo.json Includes the production-counting flag in task environments.
apps/​site/​tests/​e2e/​carbon-ads.spec.ts Tests homepage exclusion, navigation and narrow screens.
apps/​site/​next.constants.mjs Defines the API endpoint and production flag.
apps/​site/​layouts/​Post.tsx Adds the blog-post Top Bar.
apps/​site/​layouts/​Blog.tsx Adds the blog-listing Top Bar.
apps/​site/​layouts/​ArticlePage.tsx Adds the article Top Bar.
apps/​site/​layouts/​About.tsx Adds the about-page Top Bar.
apps/​site/​hooks/​useCarbonAd.ts Implements ad fetching and view-beacon timing.
apps/​site/​hooks/​__tests__/​useCarbonAd.test.jsx Tests staging requests and beacon timing.
apps/​site/​components/​MetaBar.tsx Renders Sidebar ads through the footer slot.
apps/​site/​components/​CarbonAds/​TopBar.tsx Renders the Top Bar creative.
apps/​site/​components/​CarbonAds/​Sidebar.tsx Renders the width-gated Sidebar creative.
apps/​site/​components/​CarbonAds/​index.module.css Styles both formats and their theme colours.
.changeset/​smart-lilies-invent.md Records the MetaBar footer API addition.

Review effort: Balanced

import styles from './index.module.css';

const CarbonSidebar: FC = () => {
const { ad, ref } = useCarbonAd('CWBI6KJU', '(min-width: 1280px)');
Comment on lines +12 to +13
if (!ad) {
return null;
url.searchParams.set('ignore', 'yes');
}

fetch(url, { signal: controller.signal })

fetch(url, { signal: controller.signal })
.then(response => (response.ok ? response.json() : null))
.then(data => data?.ads?.[0]?.statlink && setAd(data.ads[0]))
Comment on lines +63 to +84
let timer: ReturnType<typeof setTimeout>;

const observer = new IntersectionObserver(
entries => {
clearTimeout(timer);

if (entries.at(-1)!.intersectionRatio >= 0.5) {
timer = setTimeout(() => {
new Image().src = ad.statview;
observer.disconnect();
}, 1000);
}
},
{ threshold: 0.5 }
);

observer.observe(ref.current);

return () => {
clearTimeout(timer);
observer.disconnect();
};
Comment thread docs/carbon-ads.md
Comment on lines +17 to +18
| `download` | **No** | **No** |
| `download-archive` | **No** | **No** |

This branch was successfully deployed

2 active deployments
Preview — da292f12 Deployed Nov 3, 2026 by vercel[bot]
Storybook — da292f12 Deployed Nov 3, 2026 by bmuenzenmeyer via Chromatic #2434
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants