Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
29 changes: 20 additions & 9 deletions apps/web/public/static/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -778,17 +778,18 @@ a:hover {

/* ------------------------------------------------------------------- ads */

/* One sponsored line, sold through CrawlProof. Sized here rather than by an ad
script, so it occupies the same space before and after it loads and moves
nothing underneath it.
/* One ad, sold through CrawlProof. Sized here rather than by an ad script, so
it occupies the same space before and after it loads and moves nothing
underneath it.

An unsold slot answers with a blank document, and nothing on this side can
see that across an origin — so the unit is styled as a strip of whitespace
with no border, no ground and no label of its own. Empty, it reads as the gap
between two sections, which is where it was put. */
see that across an origin — so the unit carries no border and no ground of
its own. Empty, it is whitespace in a gap between two sections, which is
where it was put. */
.ad-unit {
display: block;
margin: 2.6rem 0;
text-align: center;
}
.ad-unit iframe {
display: block;
Expand All @@ -801,12 +802,22 @@ a:hover {
width: 100%;
height: 40px;
}
/* The banner creatives are laid out at a fixed pixel width inside the frame, so
a narrowed frame would crop one rather than reflow it. They keep their own
width and centre instead. */
/* Every banner creative is laid out at its format's fixed pixel width inside
the frame, so a narrower frame crops it rather than reflowing it. They keep
their own width and centre. */
.ad-unit:not([data-ad-format="text_link"]) iframe {
margin-inline: auto;
}
/* Below this the column is narrower than the 300px rectangle — 250px on a test
page at 320px, which would have cut a fifth off the creative — so the unit
escapes the column's padding and centres against the viewport instead. The
ad does not get bigger; it just stops being cropped by a gutter. Safe against
a stray pixel because body already hides horizontal overflow. */
@media (max-width: 640px) {
.ad-unit:not([data-ad-format="text_link"]) {
margin-inline: calc(50% - 50vw);
}
}
.ad-label {
display: block;
margin-bottom: 0.3rem;
Expand Down
23 changes: 17 additions & 6 deletions apps/web/src/components/AdUnit.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,22 @@ import { config } from '@d3vices/config';
* mints a `crawlproof.visitor` id — writes nothing into this origin's local
* storage. The privacy page can go on saying what it says.
*
* The cost of having no script is that nothing here can measure the viewport,
* and every banner creative is laid out at a fixed pixel width inside the frame
* (a narrowed iframe clips it rather than reflowing it). `text_link` is the one
* format built to fill its container, so it is the default and the only one
* that belongs in a column whose width is not known in advance.
* The cost of having no script is that nothing here can measure anything, and
* that shapes both remaining choices.
*
* The format is one fixed size for every viewport, because picking by width
* would need a script and rendering two units would bill two impressions for
* one reader. It is the rectangle: 300px is the widest fixed creative that
* still fits a 320px phone, and every creative is laid out at its format's
* exact pixel width inside the frame, so a narrower column crops it instead of
* reflowing it. The stylesheet handles the columns that are narrower than that
* by letting the unit escape their padding — see `.ad-unit` there.
*
* The theme is not passed at all. `/api/ads/frame` defaults to shipping both
* palettes behind a `prefers-color-scheme` query, which the frame answers from
* the reader's own browser — a better signal than anything this end could
* guess, given the site's theme is itself a stored preference the server never
* sees.
*/
const SIZES = {
banner_300x250: [300, 250],
Expand All @@ -30,7 +41,7 @@ const SIZES = {
text_link: [null, 40],
};

export function AdUnit({ format = 'text_link' }) {
export function AdUnit({ format = 'banner_300x250' }) {
const { origin, slot } = config.ads;
// No slot configured — the desktop export, and any checkout that has not set
// one — renders nothing at all rather than an empty box.
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/pages/TestPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export function TestPage({ test }) {

{/* Below the instrument, its permissions and its FAQ: the reading is
done and the next thing on the page is a list of links away, so a
sponsored line here interrupts nothing. Never above the test — a
unit here interrupts nothing. Never above the test — a
reader who came to find out whether their microphone works should
reach it without passing an ad. */}
<AdUnit />
Expand Down
27 changes: 23 additions & 4 deletions test/routes.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -293,10 +293,29 @@ describe('the ad unit', () => {
expect(html).not.toContain('data-cp-ad');
});

test('it asks for the one format that is actually fluid', async () => {
// Every banner creative is laid out at its format's fixed pixel width, so a
// narrower frame clips it instead of reflowing.
expect(await (await get('/')).text()).toContain('format=text_link');
test('it asks for one format on every viewport, and reserves that exact box', async () => {
// Picking a size by width would need a script, and rendering two units so
// CSS can hide one bills two impressions for one reader. So it is one fixed
// format everywhere — the rectangle, 300px being the widest creative that
// still fits a 320px phone.
//
// The box has to match the format: every creative is laid out at its
// format's exact pixel width inside the frame, so a frame of any other size
// crops it rather than reflowing it.
const tag = (await (await get('/')).text()).match(/<iframe[^>]*>/)?.[0] ?? '';
expect(tag).toContain('format=banner_300x250');
expect(tag).toContain('width="300"');
expect(tag).toContain('height="250"');
});

test('it does not ask for a theme, because this end cannot know one', async () => {
// The site's theme is a stored preference the server never sees. The frame
// defaults to shipping both palettes behind prefers-color-scheme, which it
// answers from the reader's own browser — so naming one here would be us
// overriding a better signal with a guess.
const tag = (await (await get('/')).text()).match(/<iframe[^>]*>/)?.[0] ?? '';
expect(tag).toContain('/api/ads/frame');
expect(tag).not.toContain('theme=');
});

test('the frame is sandboxed to opening its own link and nothing else', async () => {
Expand Down
Loading